background.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. // background.js — Service Worker: orchestration, state, tab management, message routing
  2. importScripts('data/names.js');
  3. const LOG_PREFIX = '[MultiPage:bg]';
  4. // ============================================================
  5. // State Management (chrome.storage.session)
  6. // ============================================================
  7. const DEFAULT_STATE = {
  8. currentStep: 0,
  9. stepStatuses: {
  10. 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending',
  11. 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
  12. },
  13. oauthUrl: null,
  14. email: null,
  15. password: null,
  16. accounts: [], // { email, password, createdAt }
  17. lastEmailTimestamp: null,
  18. localhostUrl: null,
  19. flowStartTime: null,
  20. tabRegistry: {},
  21. logs: [],
  22. vpsUrl: '',
  23. mailProvider: '163', // 'qq' or '163'
  24. };
  25. async function getState() {
  26. const state = await chrome.storage.session.get(null);
  27. return { ...DEFAULT_STATE, ...state };
  28. }
  29. async function setState(updates) {
  30. console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
  31. await chrome.storage.session.set(updates);
  32. }
  33. async function resetState() {
  34. console.log(LOG_PREFIX, 'Resetting all state');
  35. // Preserve settings and persistent data across resets
  36. const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry', 'vpsUrl', 'mailProvider']);
  37. await chrome.storage.session.clear();
  38. await chrome.storage.session.set({
  39. ...DEFAULT_STATE,
  40. seenCodes: prev.seenCodes || [],
  41. accounts: prev.accounts || [],
  42. tabRegistry: prev.tabRegistry || {},
  43. vpsUrl: prev.vpsUrl || '',
  44. mailProvider: prev.mailProvider || '163',
  45. });
  46. }
  47. /**
  48. * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
  49. */
  50. function generatePassword() {
  51. const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  52. const lower = 'abcdefghjkmnpqrstuvwxyz';
  53. const digits = '23456789';
  54. const symbols = '!@#$%&*?';
  55. const all = upper + lower + digits + symbols;
  56. // Ensure at least one of each type
  57. let pw = '';
  58. pw += upper[Math.floor(Math.random() * upper.length)];
  59. pw += lower[Math.floor(Math.random() * lower.length)];
  60. pw += digits[Math.floor(Math.random() * digits.length)];
  61. pw += symbols[Math.floor(Math.random() * symbols.length)];
  62. // Fill remaining 10 chars
  63. for (let i = 0; i < 10; i++) {
  64. pw += all[Math.floor(Math.random() * all.length)];
  65. }
  66. // Shuffle
  67. return pw.split('').sort(() => Math.random() - 0.5).join('');
  68. }
  69. // ============================================================
  70. // Tab Registry
  71. // ============================================================
  72. async function getTabRegistry() {
  73. const state = await getState();
  74. return state.tabRegistry || {};
  75. }
  76. async function registerTab(source, tabId) {
  77. const registry = await getTabRegistry();
  78. registry[source] = { tabId, ready: true };
  79. await setState({ tabRegistry: registry });
  80. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  81. }
  82. async function isTabAlive(source) {
  83. const registry = await getTabRegistry();
  84. const entry = registry[source];
  85. if (!entry) return false;
  86. try {
  87. await chrome.tabs.get(entry.tabId);
  88. return true;
  89. } catch {
  90. // Tab no longer exists — clean up registry
  91. registry[source] = null;
  92. await setState({ tabRegistry: registry });
  93. return false;
  94. }
  95. }
  96. async function getTabId(source) {
  97. const registry = await getTabRegistry();
  98. return registry[source]?.tabId || null;
  99. }
  100. // ============================================================
  101. // Command Queue (for content scripts not yet ready)
  102. // ============================================================
  103. const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
  104. function queueCommand(source, message, timeout = 15000) {
  105. return new Promise((resolve, reject) => {
  106. const timer = setTimeout(() => {
  107. pendingCommands.delete(source);
  108. const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
  109. console.error(LOG_PREFIX, err);
  110. reject(new Error(err));
  111. }, timeout);
  112. pendingCommands.set(source, { message, resolve, reject, timer });
  113. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  114. });
  115. }
  116. function flushCommand(source, tabId) {
  117. const pending = pendingCommands.get(source);
  118. if (pending) {
  119. clearTimeout(pending.timer);
  120. pendingCommands.delete(source);
  121. chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
  122. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  123. }
  124. }
  125. // ============================================================
  126. // Reuse or create tab
  127. // ============================================================
  128. async function reuseOrCreateTab(source, url, options = {}) {
  129. const alive = await isTabAlive(source);
  130. if (alive) {
  131. const tabId = await getTabId(source);
  132. // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
  133. const registry = await getTabRegistry();
  134. if (registry[source]) registry[source].ready = false;
  135. await setState({ tabRegistry: registry });
  136. // Navigate existing tab to new URL
  137. await chrome.tabs.update(tabId, { url, active: true });
  138. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
  139. // Wait for page load complete (with 30s timeout)
  140. await new Promise((resolve) => {
  141. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  142. const listener = (tid, info) => {
  143. if (tid === tabId && info.status === 'complete') {
  144. chrome.tabs.onUpdated.removeListener(listener);
  145. clearTimeout(timer);
  146. resolve();
  147. }
  148. };
  149. chrome.tabs.onUpdated.addListener(listener);
  150. });
  151. // If dynamic injection needed (VPS panel), re-inject after navigation
  152. if (options.inject) {
  153. await chrome.scripting.executeScript({
  154. target: { tabId },
  155. files: options.inject,
  156. });
  157. }
  158. // Wait a bit for content script to inject and send READY
  159. await new Promise(r => setTimeout(r, 500));
  160. return tabId;
  161. }
  162. // Create new tab
  163. const tab = await chrome.tabs.create({ url, active: true });
  164. console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
  165. // If dynamic injection needed (VPS panel), inject scripts after load
  166. if (options.inject) {
  167. await new Promise((resolve) => {
  168. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  169. const listener = (tabId, info) => {
  170. if (tabId === tab.id && info.status === 'complete') {
  171. chrome.tabs.onUpdated.removeListener(listener);
  172. clearTimeout(timer);
  173. resolve();
  174. }
  175. };
  176. chrome.tabs.onUpdated.addListener(listener);
  177. });
  178. await chrome.scripting.executeScript({
  179. target: { tabId: tab.id },
  180. files: options.inject,
  181. });
  182. }
  183. return tab.id;
  184. }
  185. // ============================================================
  186. // Send command to content script (with readiness check)
  187. // ============================================================
  188. async function sendToContentScript(source, message) {
  189. const registry = await getTabRegistry();
  190. const entry = registry[source];
  191. if (!entry || !entry.ready) {
  192. console.log(LOG_PREFIX, `${source} not ready, queuing command`);
  193. return queueCommand(source, message);
  194. }
  195. // Verify tab is still alive
  196. const alive = await isTabAlive(source);
  197. if (!alive) {
  198. // Tab was closed — queue the command, it will be sent when tab is reopened
  199. console.log(LOG_PREFIX, `${source} tab was closed, queuing command`);
  200. return queueCommand(source, message);
  201. }
  202. console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
  203. return chrome.tabs.sendMessage(entry.tabId, message);
  204. }
  205. // ============================================================
  206. // Logging
  207. // ============================================================
  208. async function addLog(message, level = 'info') {
  209. const state = await getState();
  210. const logs = state.logs || [];
  211. const entry = { message, level, timestamp: Date.now() };
  212. logs.push(entry);
  213. // Keep last 500 logs
  214. if (logs.length > 500) logs.splice(0, logs.length - 500);
  215. await setState({ logs });
  216. // Broadcast to side panel
  217. chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
  218. }
  219. // ============================================================
  220. // Step Status Management
  221. // ============================================================
  222. async function setStepStatus(step, status) {
  223. const state = await getState();
  224. const statuses = { ...state.stepStatuses };
  225. statuses[step] = status;
  226. await setState({ stepStatuses: statuses, currentStep: step });
  227. // Broadcast to side panel
  228. chrome.runtime.sendMessage({
  229. type: 'STEP_STATUS_CHANGED',
  230. payload: { step, status },
  231. }).catch(() => {});
  232. }
  233. // ============================================================
  234. // Message Handler (central router)
  235. // ============================================================
  236. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  237. console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
  238. handleMessage(message, sender).then(response => {
  239. sendResponse(response);
  240. }).catch(err => {
  241. console.error(LOG_PREFIX, 'Handler error:', err);
  242. sendResponse({ error: err.message });
  243. });
  244. return true; // async response
  245. });
  246. async function handleMessage(message, sender) {
  247. switch (message.type) {
  248. case 'CONTENT_SCRIPT_READY': {
  249. const tabId = sender.tab?.id;
  250. if (tabId && message.source) {
  251. await registerTab(message.source, tabId);
  252. flushCommand(message.source, tabId);
  253. await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
  254. }
  255. return { ok: true };
  256. }
  257. case 'LOG': {
  258. const { message: msg, level } = message.payload;
  259. await addLog(`[${message.source}] ${msg}`, level);
  260. return { ok: true };
  261. }
  262. case 'STEP_COMPLETE': {
  263. await setStepStatus(message.step, 'completed');
  264. await addLog(`Step ${message.step} completed`, 'ok');
  265. await handleStepData(message.step, message.payload);
  266. notifyStepComplete(message.step, message.payload);
  267. return { ok: true };
  268. }
  269. case 'STEP_ERROR': {
  270. await setStepStatus(message.step, 'failed');
  271. await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
  272. notifyStepError(message.step, message.error);
  273. return { ok: true };
  274. }
  275. case 'GET_STATE': {
  276. return await getState();
  277. }
  278. case 'RESET': {
  279. await resetState();
  280. await addLog('Flow reset', 'info');
  281. return { ok: true };
  282. }
  283. case 'EXECUTE_STEP': {
  284. const step = message.payload.step;
  285. // Save email if provided (from side panel step 3)
  286. if (message.payload.email) {
  287. await setState({ email: message.payload.email });
  288. }
  289. await executeStep(step);
  290. return { ok: true };
  291. }
  292. case 'AUTO_RUN': {
  293. const totalRuns = message.payload?.totalRuns || 1;
  294. autoRunLoop(totalRuns); // fire-and-forget
  295. return { ok: true };
  296. }
  297. case 'RESUME_AUTO_RUN': {
  298. if (message.payload.email) {
  299. await setState({ email: message.payload.email });
  300. }
  301. resumeAutoRun(); // fire-and-forget
  302. return { ok: true };
  303. }
  304. case 'SAVE_SETTING': {
  305. const updates = {};
  306. if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
  307. if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
  308. await setState(updates);
  309. return { ok: true };
  310. }
  311. // Side panel data updates
  312. case 'SAVE_EMAIL': {
  313. await setState({ email: message.payload.email });
  314. return { ok: true };
  315. }
  316. default:
  317. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  318. return { error: `Unknown message type: ${message.type}` };
  319. }
  320. }
  321. // ============================================================
  322. // Step Data Handlers
  323. // ============================================================
  324. async function handleStepData(step, payload) {
  325. switch (step) {
  326. case 1:
  327. if (payload.oauthUrl) {
  328. await setState({ oauthUrl: payload.oauthUrl });
  329. // Broadcast OAuth URL to side panel
  330. chrome.runtime.sendMessage({
  331. type: 'DATA_UPDATED',
  332. payload: { oauthUrl: payload.oauthUrl },
  333. }).catch(() => {});
  334. }
  335. break;
  336. case 3:
  337. if (payload.email) await setState({ email: payload.email });
  338. break;
  339. case 4:
  340. if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
  341. break;
  342. case 8:
  343. if (payload.localhostUrl) {
  344. await setState({ localhostUrl: payload.localhostUrl });
  345. chrome.runtime.sendMessage({
  346. type: 'DATA_UPDATED',
  347. payload: { localhostUrl: payload.localhostUrl },
  348. }).catch(() => {});
  349. }
  350. break;
  351. }
  352. }
  353. // ============================================================
  354. // Step Completion Waiting
  355. // ============================================================
  356. // Map of step -> { resolve, reject } for waiting on step completion
  357. const stepWaiters = new Map();
  358. function waitForStepComplete(step, timeoutMs = 120000) {
  359. return new Promise((resolve, reject) => {
  360. const timer = setTimeout(() => {
  361. stepWaiters.delete(step);
  362. reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
  363. }, timeoutMs);
  364. stepWaiters.set(step, {
  365. resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
  366. reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
  367. });
  368. });
  369. }
  370. function notifyStepComplete(step, payload) {
  371. const waiter = stepWaiters.get(step);
  372. if (waiter) waiter.resolve(payload);
  373. }
  374. function notifyStepError(step, error) {
  375. const waiter = stepWaiters.get(step);
  376. if (waiter) waiter.reject(new Error(error));
  377. }
  378. // ============================================================
  379. // Step Execution
  380. // ============================================================
  381. async function executeStep(step) {
  382. console.log(LOG_PREFIX, `Executing step ${step}`);
  383. await setStepStatus(step, 'running');
  384. await addLog(`Step ${step} started`);
  385. const state = await getState();
  386. // Set flow start time on first step
  387. if (step === 1 && !state.flowStartTime) {
  388. await setState({ flowStartTime: Date.now() });
  389. }
  390. try {
  391. switch (step) {
  392. case 1: await executeStep1(state); break;
  393. case 2: await executeStep2(state); break;
  394. case 3: await executeStep3(state); break;
  395. case 4: await executeStep4(state); break;
  396. case 5: await executeStep5(state); break;
  397. case 6: await executeStep6(state); break;
  398. case 7: await executeStep7(state); break;
  399. case 8: await executeStep8(state); break;
  400. case 9: await executeStep9(state); break;
  401. default:
  402. throw new Error(`Unknown step: ${step}`);
  403. }
  404. } catch (err) {
  405. await setStepStatus(step, 'failed');
  406. await addLog(`Step ${step} failed: ${err.message}`, 'error');
  407. }
  408. }
  409. /**
  410. * Execute a step and wait for it to complete before returning.
  411. * @param {number} step
  412. * @param {number} delayAfter - ms to wait after completion (for page transitions)
  413. */
  414. async function executeStepAndWait(step, delayAfter = 2000) {
  415. const promise = waitForStepComplete(step, 120000);
  416. await executeStep(step);
  417. await promise;
  418. // Extra delay for page transitions / DOM updates
  419. if (delayAfter > 0) {
  420. await new Promise(r => setTimeout(r, delayAfter));
  421. }
  422. }
  423. // ============================================================
  424. // Auto Run Flow
  425. // ============================================================
  426. let autoRunActive = false;
  427. let autoRunCurrentRun = 0;
  428. let autoRunTotalRuns = 1;
  429. // Outer loop: runs the full flow N times
  430. async function autoRunLoop(totalRuns) {
  431. if (autoRunActive) {
  432. await addLog('Auto run already in progress', 'warn');
  433. return;
  434. }
  435. autoRunActive = true;
  436. autoRunTotalRuns = totalRuns;
  437. await setState({ autoRunning: true });
  438. for (let run = 1; run <= totalRuns; run++) {
  439. autoRunCurrentRun = run;
  440. // Reset everything at the start of each run (keep VPS/mail settings)
  441. const prevState = await getState();
  442. const keepSettings = {
  443. vpsUrl: prevState.vpsUrl,
  444. mailProvider: prevState.mailProvider,
  445. autoRunning: true,
  446. };
  447. await resetState();
  448. await setState(keepSettings);
  449. // Tell side panel to reset all UI
  450. chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
  451. await new Promise(r => setTimeout(r, 500));
  452. await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
  453. const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } });
  454. try {
  455. chrome.runtime.sendMessage(status('running')).catch(() => {});
  456. await executeStepAndWait(1, 2000);
  457. await executeStepAndWait(2, 2000);
  458. // Pause for email
  459. await addLog(`=== Run ${run}/${totalRuns} PAUSED: Paste DuckDuckGo email, click Continue ===`, 'warn');
  460. chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
  461. // Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
  462. await waitForResume();
  463. const state = await getState();
  464. if (!state.email) {
  465. await addLog('Cannot resume: no email address.', 'error');
  466. break;
  467. }
  468. await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
  469. chrome.runtime.sendMessage(status('running')).catch(() => {});
  470. await executeStepAndWait(3, 3000);
  471. await executeStepAndWait(4, 2000);
  472. await executeStepAndWait(5, 3000);
  473. await executeStepAndWait(6, 3000);
  474. await executeStepAndWait(7, 2000);
  475. await executeStepAndWait(8, 2000);
  476. await executeStepAndWait(9, 1000);
  477. await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
  478. } catch (err) {
  479. await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
  480. chrome.runtime.sendMessage(status('stopped')).catch(() => {});
  481. break; // Stop on error
  482. }
  483. }
  484. const completedRuns = autoRunCurrentRun;
  485. if (completedRuns >= autoRunTotalRuns) {
  486. await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok');
  487. } else {
  488. await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
  489. }
  490. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
  491. autoRunActive = false;
  492. await setState({ autoRunning: false });
  493. }
  494. // Promise-based pause/resume mechanism
  495. let resumeResolver = null;
  496. function waitForResume() {
  497. return new Promise((resolve) => {
  498. resumeResolver = resolve;
  499. });
  500. }
  501. async function resumeAutoRun() {
  502. const state = await getState();
  503. if (!state.email) {
  504. await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
  505. return;
  506. }
  507. if (resumeResolver) {
  508. resumeResolver();
  509. resumeResolver = null;
  510. }
  511. }
  512. // ============================================================
  513. // Step 1: Get OAuth Link (via vps-panel.js)
  514. // ============================================================
  515. async function executeStep1(state) {
  516. if (!state.vpsUrl) {
  517. throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
  518. }
  519. await addLog(`Step 1: Opening VPS panel...`);
  520. await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'] });
  521. await sendToContentScript('vps-panel', {
  522. type: 'EXECUTE_STEP',
  523. step: 1,
  524. source: 'background',
  525. payload: {},
  526. });
  527. }
  528. // ============================================================
  529. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  530. // ============================================================
  531. async function executeStep2(state) {
  532. if (!state.oauthUrl) {
  533. throw new Error('No OAuth URL. Complete step 1 first.');
  534. }
  535. await addLog(`Step 2: Opening auth URL...`);
  536. await reuseOrCreateTab('signup-page', state.oauthUrl);
  537. await sendToContentScript('signup-page', {
  538. type: 'EXECUTE_STEP',
  539. step: 2,
  540. source: 'background',
  541. payload: {},
  542. });
  543. }
  544. // ============================================================
  545. // Step 3: Fill Email & Password (via signup-page.js)
  546. // ============================================================
  547. async function executeStep3(state) {
  548. if (!state.email) {
  549. throw new Error('No email address. Paste email in Side Panel first.');
  550. }
  551. // Generate a unique password for this account
  552. const password = generatePassword();
  553. await setState({ password });
  554. // Save account record
  555. const accounts = state.accounts || [];
  556. accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
  557. await setState({ accounts });
  558. await addLog(`Step 3: Filling email ${state.email}, password generated (${password.length} chars)`);
  559. await sendToContentScript('signup-page', {
  560. type: 'EXECUTE_STEP',
  561. step: 3,
  562. source: 'background',
  563. payload: { email: state.email, password },
  564. });
  565. }
  566. // ============================================================
  567. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  568. // ============================================================
  569. function getMailConfig(state) {
  570. const provider = state.mailProvider || 'qq';
  571. if (provider === '163') {
  572. 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' };
  573. }
  574. return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
  575. }
  576. async function executeStep4(state) {
  577. const mail = getMailConfig(state);
  578. await addLog(`Step 4: Opening ${mail.label}...`);
  579. // For mail tabs, only create if not alive — don't navigate (preserves login session)
  580. const alive = await isTabAlive(mail.source);
  581. if (alive) {
  582. const tabId = await getTabId(mail.source);
  583. await chrome.tabs.update(tabId, { active: true });
  584. } else {
  585. await reuseOrCreateTab(mail.source, mail.url);
  586. }
  587. const result = await sendToContentScript(mail.source, {
  588. type: 'POLL_EMAIL',
  589. step: 4,
  590. source: 'background',
  591. payload: {
  592. filterAfterTimestamp: state.flowStartTime || 0,
  593. senderFilters: ['openai', 'noreply', 'verify', 'auth'],
  594. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
  595. maxAttempts: 20,
  596. intervalMs: 3000,
  597. },
  598. });
  599. if (result && result.error) {
  600. throw new Error(result.error);
  601. }
  602. if (result && result.code) {
  603. await setState({ lastEmailTimestamp: result.emailTimestamp });
  604. await addLog(`Step 4: Got verification code: ${result.code}`);
  605. // Switch to signup tab and fill code
  606. const signupTabId = await getTabId('signup-page');
  607. if (signupTabId) {
  608. await chrome.tabs.update(signupTabId, { active: true });
  609. await sendToContentScript('signup-page', {
  610. type: 'FILL_CODE',
  611. step: 4,
  612. source: 'background',
  613. payload: { code: result.code },
  614. });
  615. } else {
  616. throw new Error('Signup page tab was closed. Cannot fill verification code.');
  617. }
  618. }
  619. }
  620. // ============================================================
  621. // Step 5: Fill Name & Birthday (via signup-page.js)
  622. // ============================================================
  623. async function executeStep5(state) {
  624. const { firstName, lastName } = generateRandomName();
  625. const { year, month, day } = generateRandomBirthday();
  626. await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
  627. await sendToContentScript('signup-page', {
  628. type: 'EXECUTE_STEP',
  629. step: 5,
  630. source: 'background',
  631. payload: { firstName, lastName, year, month, day },
  632. });
  633. }
  634. // ============================================================
  635. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  636. // ============================================================
  637. async function executeStep6(state) {
  638. if (!state.oauthUrl) {
  639. throw new Error('No OAuth URL. Complete step 1 first.');
  640. }
  641. if (!state.email) {
  642. throw new Error('No email. Complete step 3 first.');
  643. }
  644. await addLog(`Step 6: Opening OAuth URL for login...`);
  645. // Reuse the signup-page tab — navigate it to the OAuth URL
  646. await reuseOrCreateTab('signup-page', state.oauthUrl);
  647. // signup-page.js will inject (same auth.openai.com domain) and handle login
  648. await sendToContentScript('signup-page', {
  649. type: 'EXECUTE_STEP',
  650. step: 6,
  651. source: 'background',
  652. payload: { email: state.email, password: state.password },
  653. });
  654. }
  655. // ============================================================
  656. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  657. // ============================================================
  658. async function executeStep7(state) {
  659. const mail = getMailConfig(state);
  660. await addLog(`Step 7: Opening ${mail.label}...`);
  661. const alive = await isTabAlive(mail.source);
  662. if (alive) {
  663. const tabId = await getTabId(mail.source);
  664. await chrome.tabs.update(tabId, { active: true });
  665. } else {
  666. await reuseOrCreateTab(mail.source, mail.url);
  667. }
  668. const result = await sendToContentScript(mail.source, {
  669. type: 'POLL_EMAIL',
  670. step: 7,
  671. source: 'background',
  672. payload: {
  673. filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
  674. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'],
  675. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  676. maxAttempts: 20,
  677. intervalMs: 3000,
  678. },
  679. });
  680. if (result && result.error) {
  681. throw new Error(result.error);
  682. }
  683. if (result && result.code) {
  684. await addLog(`Step 7: Got login verification code: ${result.code}`);
  685. // Switch to signup/auth tab and fill code
  686. const signupTabId = await getTabId('signup-page');
  687. if (signupTabId) {
  688. await chrome.tabs.update(signupTabId, { active: true });
  689. await sendToContentScript('signup-page', {
  690. type: 'FILL_CODE',
  691. step: 7,
  692. source: 'background',
  693. payload: { code: result.code },
  694. });
  695. } else {
  696. throw new Error('Auth page tab was closed. Cannot fill verification code.');
  697. }
  698. }
  699. }
  700. // ============================================================
  701. // Step 8: Complete OAuth (webNavigation listener + chatgpt.js navigates)
  702. // ============================================================
  703. let webNavListener = null;
  704. async function executeStep8(state) {
  705. if (!state.oauthUrl) {
  706. throw new Error('No OAuth URL. Complete step 1 first.');
  707. }
  708. await addLog('Step 8: Setting up localhost redirect listener...');
  709. // Register webNavigation listener (scoped to this step)
  710. return new Promise((resolve, reject) => {
  711. const timeout = setTimeout(() => {
  712. if (webNavListener) {
  713. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  714. webNavListener = null;
  715. }
  716. setStepStatus(8, 'failed');
  717. addLog('Step 8: Localhost redirect not captured after 30s. Check if OAuth authorization completed.', 'error');
  718. reject(new Error('Localhost redirect not captured after 30s. Check if OAuth authorization completed.'));
  719. }, 30000);
  720. webNavListener = (details) => {
  721. if (details.url.startsWith('http://localhost')) {
  722. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  723. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  724. webNavListener = null;
  725. clearTimeout(timeout);
  726. setState({ localhostUrl: details.url }).then(() => {
  727. addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
  728. setStepStatus(8, 'completed');
  729. notifyStepComplete(8, { localhostUrl: details.url });
  730. chrome.runtime.sendMessage({
  731. type: 'DATA_UPDATED',
  732. payload: { localhostUrl: details.url },
  733. }).catch(() => {});
  734. resolve();
  735. });
  736. }
  737. };
  738. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  739. // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
  740. // with a "继续" button. We need to click it, which triggers the localhost redirect.
  741. (async () => {
  742. try {
  743. const signupTabId = await getTabId('signup-page');
  744. if (signupTabId) {
  745. await chrome.tabs.update(signupTabId, { active: true });
  746. await addLog('Step 8: Switching to auth page, clicking "继续" to complete OAuth...');
  747. await sendToContentScript('signup-page', {
  748. type: 'EXECUTE_STEP',
  749. step: 8,
  750. source: 'background',
  751. payload: {},
  752. });
  753. } else {
  754. await reuseOrCreateTab('signup-page', state.oauthUrl);
  755. await addLog('Step 8: Auth tab reopened...');
  756. await sendToContentScript('signup-page', {
  757. type: 'EXECUTE_STEP',
  758. step: 8,
  759. source: 'background',
  760. payload: {},
  761. });
  762. }
  763. } catch (err) {
  764. clearTimeout(timeout);
  765. if (webNavListener) {
  766. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  767. webNavListener = null;
  768. }
  769. reject(err);
  770. }
  771. })();
  772. });
  773. }
  774. // ============================================================
  775. // Step 9: VPS Verify (via vps-panel.js)
  776. // ============================================================
  777. async function executeStep9(state) {
  778. if (!state.localhostUrl) {
  779. throw new Error('No localhost URL. Complete step 8 first.');
  780. }
  781. if (!state.vpsUrl) {
  782. throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
  783. }
  784. await addLog('Step 9: Opening VPS panel...');
  785. let tabId = await getTabId('vps-panel');
  786. const alive = tabId && await isTabAlive('vps-panel');
  787. if (!alive) {
  788. // Create new tab
  789. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
  790. tabId = tab.id;
  791. await new Promise(resolve => {
  792. const listener = (tid, info) => {
  793. if (tid === tabId && info.status === 'complete') {
  794. chrome.tabs.onUpdated.removeListener(listener);
  795. resolve();
  796. }
  797. };
  798. chrome.tabs.onUpdated.addListener(listener);
  799. });
  800. } else {
  801. await chrome.tabs.update(tabId, { active: true });
  802. }
  803. // Inject scripts directly and wait for them to be ready
  804. await chrome.scripting.executeScript({
  805. target: { tabId },
  806. files: ['content/utils.js', 'content/vps-panel.js'],
  807. });
  808. await new Promise(r => setTimeout(r, 1000));
  809. // Send command directly — bypass queue/ready mechanism
  810. await addLog(`Step 9: Filling callback URL...`);
  811. await chrome.tabs.sendMessage(tabId, {
  812. type: 'EXECUTE_STEP',
  813. step: 9,
  814. source: 'background',
  815. payload: { localhostUrl: state.localhostUrl },
  816. });
  817. }
  818. // ============================================================
  819. // Open Side Panel on extension icon click
  820. // ============================================================
  821. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });