background.js 38 KB

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