background.js 40 KB

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