background.js 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095
  1. // background.js — Service Worker: orchestration, state, tab management, message routing
  2. importScripts('data/names.js');
  3. const LOG_PREFIX = '[MultiPage:bg]';
  4. const YIBASI_DOMAIN = 'yibasi.com';
  5. const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
  6. const HUMAN_STEP_DELAY_MIN = 700;
  7. const HUMAN_STEP_DELAY_MAX = 2200;
  8. initializeSessionStorageAccess();
  9. let automationWindowId = null;
  10. async function ensureAutomationWindowId() {
  11. if (automationWindowId != null) {
  12. try {
  13. await chrome.windows.get(automationWindowId);
  14. return automationWindowId;
  15. } catch {
  16. automationWindowId = null;
  17. }
  18. }
  19. const registry = await getTabRegistry();
  20. for (const entry of Object.values(registry)) {
  21. if (entry && entry.tabId) {
  22. try {
  23. const tab = await chrome.tabs.get(entry.tabId);
  24. automationWindowId = tab.windowId;
  25. return automationWindowId;
  26. } catch {}
  27. }
  28. }
  29. const win = await chrome.windows.getLastFocused();
  30. automationWindowId = win.id;
  31. return automationWindowId;
  32. }
  33. // ============================================================
  34. // State Management (chrome.storage.session + local for persistence)
  35. // ============================================================
  36. // Keys that should survive extension reload (saved to chrome.storage.local)
  37. const PERSISTENT_KEYS = [
  38. 'vpsUrl', 'customPassword', 'mailProvider',
  39. 'gmailBaseEmail',
  40. 'gmailApiClientId',
  41. 'gmailApiClientSecret',
  42. 'gmailApiRefreshToken',
  43. 'gmailApiAccessToken',
  44. 'gmailApiAccessTokenExpiresAt',
  45. 'tempApiUrl',
  46. 'tempApiMailsPath',
  47. 'tempApiAdminAuth',
  48. 'tempApiCustomAuth',
  49. 'tempApiFingerprint',
  50. 'tempApiUserToken',
  51. 'inbucketHost', 'inbucketMailbox', 'accounts',
  52. 'seenCodes', 'seenInbucketMailIds',
  53. ];
  54. const DEFAULT_STATE = {
  55. currentStep: 0,
  56. stepStatuses: {
  57. 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending',
  58. 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
  59. },
  60. oauthUrl: null,
  61. email: null,
  62. password: null,
  63. accounts: [], // { email, password, createdAt }
  64. lastEmailTimestamp: null,
  65. localhostUrl: null,
  66. flowStartTime: null,
  67. tabRegistry: {},
  68. logs: [],
  69. vpsUrl: '',
  70. customPassword: '',
  71. mailProvider: 'temp-api', // 'temp-api', 'qq', '163', 'gmail', or 'inbucket'
  72. gmailBaseEmail: 'chen6485882@gmail.com',
  73. gmailApiClientId: '',
  74. gmailApiClientSecret: '',
  75. gmailApiRefreshToken: '',
  76. gmailApiAccessToken: '',
  77. gmailApiAccessTokenExpiresAt: 0,
  78. tempApiUrl: '',
  79. tempApiMailsPath: 'admin', // 'api' | 'admin'
  80. tempApiAdminAuth: '',
  81. tempApiCustomAuth: '',
  82. tempApiFingerprint: '',
  83. tempApiUserToken: '',
  84. inbucketHost: '',
  85. inbucketMailbox: '',
  86. };
  87. async function getState() {
  88. const state = await chrome.storage.session.get(null);
  89. return { ...DEFAULT_STATE, ...state };
  90. }
  91. async function initializeSessionStorageAccess() {
  92. try {
  93. if (chrome.storage?.session?.setAccessLevel) {
  94. await chrome.storage.session.setAccessLevel({
  95. accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
  96. });
  97. console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
  98. }
  99. } catch (err) {
  100. console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
  101. }
  102. // Restore persistent settings from chrome.storage.local into session
  103. try {
  104. const saved = await chrome.storage.local.get(PERSISTENT_KEYS);
  105. const toRestore = {};
  106. for (const key of PERSISTENT_KEYS) {
  107. if (saved[key] !== undefined) toRestore[key] = saved[key];
  108. }
  109. if (Object.keys(toRestore).length > 0) {
  110. await chrome.storage.session.set(toRestore);
  111. console.log(LOG_PREFIX, 'Restored persistent settings from local storage:', Object.keys(toRestore).join(', '));
  112. }
  113. } catch (err) {
  114. console.warn(LOG_PREFIX, 'Failed to restore persistent settings:', err?.message || err);
  115. }
  116. }
  117. async function setState(updates) {
  118. console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
  119. await chrome.storage.session.set(updates);
  120. // Persist relevant keys to chrome.storage.local so they survive reload
  121. const toPersist = {};
  122. for (const key of PERSISTENT_KEYS) {
  123. if (key in updates) toPersist[key] = updates[key];
  124. }
  125. if (Object.keys(toPersist).length > 0) {
  126. await chrome.storage.local.set(toPersist);
  127. }
  128. }
  129. function broadcastDataUpdate(payload) {
  130. chrome.runtime.sendMessage({
  131. type: 'DATA_UPDATED',
  132. payload,
  133. }).catch(() => {});
  134. }
  135. async function setEmailState(email) {
  136. await setState({ email });
  137. broadcastDataUpdate({ email });
  138. }
  139. async function setPasswordState(password) {
  140. await setState({ password });
  141. broadcastDataUpdate({ password });
  142. }
  143. async function resetState() {
  144. console.log(LOG_PREFIX, 'Resetting all state');
  145. // Preserve settings and persistent data across resets
  146. const prev = await chrome.storage.session.get(PERSISTENT_KEYS.concat(['tabRegistry']));
  147. // Also check chrome.storage.local in case session was empty (after reload)
  148. const local = await chrome.storage.local.get(PERSISTENT_KEYS);
  149. await chrome.storage.session.clear();
  150. // Restore all persistent keys (prefer session value, fallback to local)
  151. const restored = { ...DEFAULT_STATE, tabRegistry: prev.tabRegistry || {} };
  152. for (const key of PERSISTENT_KEYS) {
  153. if (prev[key] !== undefined && prev[key] !== null && prev[key] !== '') {
  154. restored[key] = prev[key];
  155. } else if (local[key] !== undefined && local[key] !== null && local[key] !== '') {
  156. restored[key] = local[key];
  157. }
  158. }
  159. await chrome.storage.session.set(restored);
  160. }
  161. /**
  162. * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
  163. */
  164. function generatePassword() {
  165. const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  166. const lower = 'abcdefghjkmnpqrstuvwxyz';
  167. const digits = '23456789';
  168. const symbols = '!@#$%&*?';
  169. const all = upper + lower + digits + symbols;
  170. // Ensure at least one of each type
  171. let pw = '';
  172. pw += upper[Math.floor(Math.random() * upper.length)];
  173. pw += lower[Math.floor(Math.random() * lower.length)];
  174. pw += digits[Math.floor(Math.random() * digits.length)];
  175. pw += symbols[Math.floor(Math.random() * symbols.length)];
  176. // Fill remaining 10 chars
  177. for (let i = 0; i < 10; i++) {
  178. pw += all[Math.floor(Math.random() * all.length)];
  179. }
  180. // Shuffle
  181. return pw.split('').sort(() => Math.random() - 0.5).join('');
  182. }
  183. // ============================================================
  184. // Tab Registry
  185. // ============================================================
  186. async function getTabRegistry() {
  187. const state = await getState();
  188. return state.tabRegistry || {};
  189. }
  190. async function registerTab(source, tabId) {
  191. const registry = await getTabRegistry();
  192. registry[source] = { tabId, ready: true };
  193. await setState({ tabRegistry: registry });
  194. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  195. }
  196. async function isTabAlive(source) {
  197. const registry = await getTabRegistry();
  198. const entry = registry[source];
  199. if (!entry) return false;
  200. try {
  201. await chrome.tabs.get(entry.tabId);
  202. return true;
  203. } catch {
  204. // Tab no longer exists — clean up registry
  205. registry[source] = null;
  206. await setState({ tabRegistry: registry });
  207. return false;
  208. }
  209. }
  210. async function getTabId(source) {
  211. const registry = await getTabRegistry();
  212. return registry[source]?.tabId || null;
  213. }
  214. // ============================================================
  215. // Command Queue (for content scripts not yet ready)
  216. // ============================================================
  217. const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
  218. function queueCommand(source, message, timeout = 15000) {
  219. return new Promise((resolve, reject) => {
  220. const timer = setTimeout(() => {
  221. pendingCommands.delete(source);
  222. const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
  223. console.error(LOG_PREFIX, err);
  224. reject(new Error(err));
  225. }, timeout);
  226. pendingCommands.set(source, { message, resolve, reject, timer });
  227. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  228. });
  229. }
  230. function flushCommand(source, tabId) {
  231. const pending = pendingCommands.get(source);
  232. if (pending) {
  233. clearTimeout(pending.timer);
  234. pendingCommands.delete(source);
  235. chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
  236. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  237. }
  238. }
  239. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  240. for (const [source, pending] of pendingCommands.entries()) {
  241. clearTimeout(pending.timer);
  242. pending.reject(new Error(reason));
  243. pendingCommands.delete(source);
  244. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  245. }
  246. }
  247. // ============================================================
  248. // Reuse or create tab
  249. // ============================================================
  250. async function reuseOrCreateTab(source, url, options = {}) {
  251. const alive = await isTabAlive(source);
  252. if (alive) {
  253. const tabId = await getTabId(source);
  254. const currentTab = await chrome.tabs.get(tabId);
  255. const sameUrl = currentTab.url === url;
  256. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  257. const registry = await getTabRegistry();
  258. if (sameUrl) {
  259. await chrome.tabs.update(tabId, { active: true });
  260. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}) on same URL`);
  261. if (shouldReloadOnReuse) {
  262. if (registry[source]) registry[source].ready = false;
  263. await setState({ tabRegistry: registry });
  264. await chrome.tabs.reload(tabId);
  265. await new Promise((resolve) => {
  266. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  267. const listener = (tid, info) => {
  268. if (tid === tabId && info.status === 'complete') {
  269. chrome.tabs.onUpdated.removeListener(listener);
  270. clearTimeout(timer);
  271. resolve();
  272. }
  273. };
  274. chrome.tabs.onUpdated.addListener(listener);
  275. });
  276. }
  277. // For dynamically injected pages like the VPS panel, re-inject immediately.
  278. if (options.inject) {
  279. if (registry[source]) registry[source].ready = false;
  280. await setState({ tabRegistry: registry });
  281. if (options.injectSource) {
  282. await chrome.scripting.executeScript({
  283. target: { tabId },
  284. func: (injectedSource) => {
  285. window.__MULTIPAGE_SOURCE = injectedSource;
  286. },
  287. args: [options.injectSource],
  288. });
  289. }
  290. await chrome.scripting.executeScript({
  291. target: { tabId },
  292. files: options.inject,
  293. });
  294. await new Promise(r => setTimeout(r, 500));
  295. }
  296. return tabId;
  297. }
  298. // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
  299. if (registry[source]) registry[source].ready = false;
  300. await setState({ tabRegistry: registry });
  301. // Navigate existing tab to new URL
  302. await chrome.tabs.update(tabId, { url, active: true });
  303. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
  304. // Wait for page load complete (with 30s timeout)
  305. await new Promise((resolve) => {
  306. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  307. const listener = (tid, info) => {
  308. if (tid === tabId && info.status === 'complete') {
  309. chrome.tabs.onUpdated.removeListener(listener);
  310. clearTimeout(timer);
  311. resolve();
  312. }
  313. };
  314. chrome.tabs.onUpdated.addListener(listener);
  315. });
  316. // If dynamic injection needed (VPS panel), re-inject after navigation
  317. if (options.inject) {
  318. if (options.injectSource) {
  319. await chrome.scripting.executeScript({
  320. target: { tabId },
  321. func: (injectedSource) => {
  322. window.__MULTIPAGE_SOURCE = injectedSource;
  323. },
  324. args: [options.injectSource],
  325. });
  326. }
  327. await chrome.scripting.executeScript({
  328. target: { tabId },
  329. files: options.inject,
  330. });
  331. }
  332. // Wait a bit for content script to inject and send READY
  333. await new Promise(r => setTimeout(r, 500));
  334. return tabId;
  335. }
  336. // Create new tab in the automation window
  337. const wid = await ensureAutomationWindowId();
  338. const tab = await chrome.tabs.create({ url, active: true, windowId: wid });
  339. console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
  340. // If dynamic injection needed (VPS panel), inject scripts after load
  341. if (options.inject) {
  342. await new Promise((resolve) => {
  343. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  344. const listener = (tabId, info) => {
  345. if (tabId === tab.id && info.status === 'complete') {
  346. chrome.tabs.onUpdated.removeListener(listener);
  347. clearTimeout(timer);
  348. resolve();
  349. }
  350. };
  351. chrome.tabs.onUpdated.addListener(listener);
  352. });
  353. if (options.injectSource) {
  354. await chrome.scripting.executeScript({
  355. target: { tabId: tab.id },
  356. func: (injectedSource) => {
  357. window.__MULTIPAGE_SOURCE = injectedSource;
  358. },
  359. args: [options.injectSource],
  360. });
  361. }
  362. await chrome.scripting.executeScript({
  363. target: { tabId: tab.id },
  364. files: options.inject,
  365. });
  366. }
  367. return tab.id;
  368. }
  369. // ============================================================
  370. // Send command to content script (with readiness check)
  371. // ============================================================
  372. async function sendToContentScript(source, message) {
  373. const registry = await getTabRegistry();
  374. const entry = registry[source];
  375. if (!entry || !entry.ready) {
  376. console.log(LOG_PREFIX, `${source} not ready, queuing command`);
  377. return queueCommand(source, message);
  378. }
  379. // Verify tab is still alive
  380. const alive = await isTabAlive(source);
  381. if (!alive) {
  382. // Tab was closed — queue the command, it will be sent when tab is reopened
  383. console.log(LOG_PREFIX, `${source} tab was closed, queuing command`);
  384. return queueCommand(source, message);
  385. }
  386. console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
  387. try {
  388. return await chrome.tabs.sendMessage(entry.tabId, message);
  389. } catch (err) {
  390. // Handle bfcache / port closed errors by reloading the tab and retrying
  391. if (err?.message?.includes('back/forward cache') || err?.message?.includes('message channel is closed')) {
  392. console.log(LOG_PREFIX, `${source} hit bfcache error, reloading tab and retrying...`);
  393. try {
  394. await chrome.tabs.reload(entry.tabId);
  395. // Wait for page to reload and content script to re-inject
  396. await new Promise((resolve) => {
  397. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 15000);
  398. const listener = (tid, info) => {
  399. if (tid === entry.tabId && info.status === 'complete') {
  400. chrome.tabs.onUpdated.removeListener(listener);
  401. clearTimeout(timer);
  402. resolve();
  403. }
  404. };
  405. chrome.tabs.onUpdated.addListener(listener);
  406. });
  407. // Mark as not ready and wait for READY signal
  408. const reg = await getTabRegistry();
  409. if (reg[source]) reg[source].ready = false;
  410. await setState({ tabRegistry: reg });
  411. // Queue the command — it'll be sent once the content script signals READY
  412. return queueCommand(source, message);
  413. } catch (retryErr) {
  414. throw new Error(`${source} bfcache recovery failed: ${retryErr.message}`);
  415. }
  416. }
  417. throw err;
  418. }
  419. }
  420. // ============================================================
  421. // Logging
  422. // ============================================================
  423. async function addLog(message, level = 'info') {
  424. const state = await getState();
  425. const logs = state.logs || [];
  426. const entry = { message, level, timestamp: Date.now() };
  427. logs.push(entry);
  428. // Keep last 500 logs
  429. if (logs.length > 500) logs.splice(0, logs.length - 500);
  430. await setState({ logs });
  431. // Broadcast to side panel
  432. chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
  433. }
  434. // ============================================================
  435. // Step Status Management
  436. // ============================================================
  437. async function setStepStatus(step, status) {
  438. const state = await getState();
  439. const statuses = { ...state.stepStatuses };
  440. statuses[step] = status;
  441. await setState({ stepStatuses: statuses, currentStep: step });
  442. // Broadcast to side panel
  443. chrome.runtime.sendMessage({
  444. type: 'STEP_STATUS_CHANGED',
  445. payload: { step, status },
  446. }).catch(() => {});
  447. }
  448. function isStopError(error) {
  449. const message = typeof error === 'string' ? error : error?.message;
  450. return message === STOP_ERROR_MESSAGE;
  451. }
  452. function clearStopRequest() {
  453. stopRequested = false;
  454. }
  455. function throwIfStopped() {
  456. if (stopRequested) {
  457. throw new Error(STOP_ERROR_MESSAGE);
  458. }
  459. }
  460. async function sleepWithStop(ms) {
  461. const start = Date.now();
  462. while (Date.now() - start < ms) {
  463. throwIfStopped();
  464. await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start))));
  465. }
  466. }
  467. async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) {
  468. const duration = Math.floor(Math.random() * (max - min + 1)) + min;
  469. await sleepWithStop(duration);
  470. }
  471. // clickWithDebugger removed — Step 8 now uses content script simulateClick directly
  472. async function broadcastStopToContentScripts() {
  473. const registry = await getTabRegistry();
  474. for (const entry of Object.values(registry)) {
  475. if (!entry?.tabId) continue;
  476. try {
  477. await chrome.tabs.sendMessage(entry.tabId, {
  478. type: 'STOP_FLOW',
  479. source: 'background',
  480. payload: {},
  481. });
  482. } catch {}
  483. }
  484. }
  485. let stopRequested = false;
  486. // ============================================================
  487. // Message Handler (central router)
  488. // ============================================================
  489. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  490. console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
  491. handleMessage(message, sender).then(response => {
  492. sendResponse(response);
  493. }).catch(err => {
  494. console.error(LOG_PREFIX, 'Handler error:', err);
  495. sendResponse({ error: err.message });
  496. });
  497. return true; // async response
  498. });
  499. async function handleMessage(message, sender) {
  500. switch (message.type) {
  501. case 'CONTENT_SCRIPT_READY': {
  502. const tabId = sender.tab?.id;
  503. if (tabId && message.source) {
  504. await registerTab(message.source, tabId);
  505. flushCommand(message.source, tabId);
  506. await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
  507. }
  508. return { ok: true };
  509. }
  510. case 'LOG': {
  511. const { message: msg, level } = message.payload;
  512. await addLog(`[${message.source}] ${msg}`, level);
  513. return { ok: true };
  514. }
  515. case 'STEP_COMPLETE': {
  516. if (stopRequested) {
  517. await setStepStatus(message.step, 'stopped');
  518. notifyStepError(message.step, STOP_ERROR_MESSAGE);
  519. return { ok: true };
  520. }
  521. await setStepStatus(message.step, 'completed');
  522. await addLog(`Step ${message.step} completed`, 'ok');
  523. await handleStepData(message.step, message.payload);
  524. notifyStepComplete(message.step, message.payload);
  525. return { ok: true };
  526. }
  527. case 'STEP_ERROR': {
  528. if (isStopError(message.error)) {
  529. await setStepStatus(message.step, 'stopped');
  530. await addLog(`Step ${message.step} stopped by user`, 'warn');
  531. notifyStepError(message.step, message.error);
  532. } else {
  533. await setStepStatus(message.step, 'failed');
  534. await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
  535. notifyStepError(message.step, message.error);
  536. }
  537. return { ok: true };
  538. }
  539. case 'GET_STATE': {
  540. return await getState();
  541. }
  542. case 'RESET': {
  543. clearStopRequest();
  544. await resetState();
  545. await addLog('Flow reset', 'info');
  546. return { ok: true };
  547. }
  548. case 'EXECUTE_STEP': {
  549. clearStopRequest();
  550. const step = message.payload.step;
  551. // Save email if provided (from side panel step 3)
  552. if (message.payload.email) {
  553. await setEmailState(message.payload.email);
  554. }
  555. await executeStep(step);
  556. return { ok: true };
  557. }
  558. case 'AUTO_RUN': {
  559. clearStopRequest();
  560. const totalRuns = message.payload?.totalRuns || 1;
  561. autoRunLoop(totalRuns); // fire-and-forget
  562. return { ok: true };
  563. }
  564. case 'RESUME_AUTO_RUN': {
  565. clearStopRequest();
  566. if (message.payload.email) {
  567. await setEmailState(message.payload.email);
  568. }
  569. resumeAutoRun(); // fire-and-forget
  570. return { ok: true };
  571. }
  572. case 'SAVE_SETTING': {
  573. const updates = {};
  574. if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
  575. if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
  576. if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
  577. if (message.payload.gmailBaseEmail !== undefined) updates.gmailBaseEmail = message.payload.gmailBaseEmail;
  578. if (message.payload.gmailApiClientId !== undefined) updates.gmailApiClientId = message.payload.gmailApiClientId;
  579. if (message.payload.gmailApiClientSecret !== undefined) updates.gmailApiClientSecret = message.payload.gmailApiClientSecret;
  580. if (message.payload.gmailApiRefreshToken !== undefined) updates.gmailApiRefreshToken = message.payload.gmailApiRefreshToken;
  581. if (message.payload.gmailApiAccessToken !== undefined) updates.gmailApiAccessToken = message.payload.gmailApiAccessToken;
  582. if (message.payload.tempApiUrl !== undefined) updates.tempApiUrl = message.payload.tempApiUrl;
  583. if (message.payload.tempApiMailsPath !== undefined) updates.tempApiMailsPath = message.payload.tempApiMailsPath;
  584. if (message.payload.tempApiAdminAuth !== undefined) updates.tempApiAdminAuth = message.payload.tempApiAdminAuth;
  585. if (message.payload.tempApiCustomAuth !== undefined) updates.tempApiCustomAuth = message.payload.tempApiCustomAuth;
  586. if (message.payload.tempApiFingerprint !== undefined) updates.tempApiFingerprint = message.payload.tempApiFingerprint;
  587. if (message.payload.tempApiUserToken !== undefined) updates.tempApiUserToken = message.payload.tempApiUserToken;
  588. if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
  589. if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
  590. await setState(updates);
  591. return { ok: true };
  592. }
  593. // Side panel data updates
  594. case 'SAVE_EMAIL': {
  595. await setEmailState(message.payload.email);
  596. return { ok: true, email: message.payload.email };
  597. }
  598. case 'GENERATE_YIBASI_EMAIL': {
  599. clearStopRequest();
  600. const email = await generateYibasiEmail();
  601. return { ok: true, email };
  602. }
  603. case 'STOP_FLOW': {
  604. await requestStop();
  605. return { ok: true };
  606. }
  607. default:
  608. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  609. return { error: `Unknown message type: ${message.type}` };
  610. }
  611. }
  612. // ============================================================
  613. // Step Data Handlers
  614. // ============================================================
  615. async function handleStepData(step, payload) {
  616. switch (step) {
  617. case 1:
  618. if (payload.oauthUrl) {
  619. await setState({ oauthUrl: payload.oauthUrl });
  620. broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
  621. }
  622. break;
  623. case 3:
  624. if (payload.email) await setEmailState(payload.email);
  625. break;
  626. case 4:
  627. if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
  628. break;
  629. case 8:
  630. if (payload.localhostUrl) {
  631. await setState({ localhostUrl: payload.localhostUrl });
  632. broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
  633. }
  634. break;
  635. }
  636. }
  637. // ============================================================
  638. // Step Completion Waiting
  639. // ============================================================
  640. // Map of step -> { resolve, reject } for waiting on step completion
  641. const stepWaiters = new Map();
  642. let resumeWaiter = null;
  643. function waitForStepComplete(step, timeoutMs = 120000) {
  644. return new Promise((resolve, reject) => {
  645. throwIfStopped();
  646. const timer = setTimeout(() => {
  647. stepWaiters.delete(step);
  648. reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
  649. }, timeoutMs);
  650. stepWaiters.set(step, {
  651. resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
  652. reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
  653. });
  654. });
  655. }
  656. function notifyStepComplete(step, payload) {
  657. const waiter = stepWaiters.get(step);
  658. if (waiter) waiter.resolve(payload);
  659. }
  660. function notifyStepError(step, error) {
  661. const waiter = stepWaiters.get(step);
  662. if (waiter) waiter.reject(new Error(error));
  663. }
  664. async function markRunningStepsStopped() {
  665. const state = await getState();
  666. const runningSteps = Object.entries(state.stepStatuses || {})
  667. .filter(([, status]) => status === 'running')
  668. .map(([step]) => Number(step));
  669. for (const step of runningSteps) {
  670. await setStepStatus(step, 'stopped');
  671. }
  672. }
  673. async function requestStop() {
  674. if (stopRequested) return;
  675. stopRequested = true;
  676. cancelPendingCommands();
  677. if (webNavListener) {
  678. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  679. webNavListener = null;
  680. }
  681. await addLog('Stop requested. Cancelling current operations...', 'warn');
  682. await broadcastStopToContentScripts();
  683. for (const waiter of stepWaiters.values()) {
  684. waiter.reject(new Error(STOP_ERROR_MESSAGE));
  685. }
  686. stepWaiters.clear();
  687. if (resumeWaiter) {
  688. resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
  689. resumeWaiter = null;
  690. }
  691. await markRunningStepsStopped();
  692. autoRunActive = false;
  693. await setState({ autoRunning: false });
  694. chrome.runtime.sendMessage({
  695. type: 'AUTO_RUN_STATUS',
  696. payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns },
  697. }).catch(() => {});
  698. }
  699. // ============================================================
  700. // Step Execution
  701. // ============================================================
  702. async function executeStep(step) {
  703. console.log(LOG_PREFIX, `Executing step ${step}`);
  704. throwIfStopped();
  705. await setStepStatus(step, 'running');
  706. await addLog(`Step ${step} started`);
  707. await humanStepDelay();
  708. const state = await getState();
  709. // Set flow start time on first step
  710. if (step === 1 && !state.flowStartTime) {
  711. await setState({ flowStartTime: Date.now() });
  712. }
  713. try {
  714. switch (step) {
  715. case 1: await executeStep1(state); break;
  716. case 2: await executeStep2(state); break;
  717. case 3: await executeStep3(state); break;
  718. case 4: await executeStep4(state); break;
  719. case 5: await executeStep5(state); break;
  720. case 6: await executeStep6(state); break;
  721. case 7: await executeStep7(state); break;
  722. case 8: await executeStep8(state); break;
  723. case 9: await executeStep9(state); break;
  724. default:
  725. throw new Error(`Unknown step: ${step}`);
  726. }
  727. } catch (err) {
  728. if (isStopError(err)) {
  729. await setStepStatus(step, 'stopped');
  730. await addLog(`Step ${step} stopped by user`, 'warn');
  731. throw err;
  732. }
  733. await setStepStatus(step, 'failed');
  734. await addLog(`Step ${step} failed: ${err.message}`, 'error');
  735. throw err;
  736. }
  737. }
  738. /**
  739. * Execute a step and wait for it to complete before returning.
  740. * @param {number} step
  741. * @param {number} delayAfter - ms to wait after completion (for page transitions)
  742. */
  743. async function executeStepAndWait(step, delayAfter = 2000) {
  744. throwIfStopped();
  745. const promise = waitForStepComplete(step, 120000);
  746. await executeStep(step);
  747. await promise;
  748. // Extra delay for page transitions / DOM updates
  749. if (delayAfter > 0) {
  750. await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
  751. }
  752. }
  753. function generateRandomLocalPart(len = 12) {
  754. const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
  755. const bytes = new Uint8Array(len);
  756. crypto.getRandomValues(bytes);
  757. let local = '';
  758. for (let i = 0; i < len; i++) {
  759. local += alphabet[bytes[i] % alphabet.length];
  760. }
  761. return local;
  762. }
  763. async function waitForContentScriptReady(source, timeoutMs = 10000) {
  764. const start = Date.now();
  765. while (Date.now() - start < timeoutMs) {
  766. const registry = await getTabRegistry();
  767. if (registry[source]?.ready) return true;
  768. await sleepWithStop(250);
  769. }
  770. return false;
  771. }
  772. function generateRandomYibasiEmail() {
  773. const local = generateRandomLocalPart(12);
  774. return `${local}@${YIBASI_DOMAIN}`;
  775. }
  776. function generateGmailAlias(baseEmail) {
  777. const trimmed = String(baseEmail || '').trim().toLowerCase();
  778. const match = trimmed.match(/^([^@+]+)(?:\+[^@]*)?@gmail\.com$/i);
  779. if (!match) {
  780. throw new Error('Gmail 基础邮箱无效,请填写一个 gmail.com 地址。');
  781. }
  782. return `${match[1]}+${generateRandomLocalPart(10)}@gmail.com`;
  783. }
  784. async function generateYibasiEmail() {
  785. throwIfStopped();
  786. const state = await getState();
  787. const email = (state.mailProvider || 'temp-api') === 'gmail'
  788. ? generateGmailAlias(state.gmailBaseEmail)
  789. : generateRandomYibasiEmail();
  790. await setEmailState(email);
  791. await addLog(`${(state.mailProvider || 'temp-api') === 'gmail' ? 'Gmail alias' : 'Yibasi'}: Generated ${email}`, 'ok');
  792. return email;
  793. }
  794. // ============================================================
  795. // Auto Run Flow
  796. // ============================================================
  797. let autoRunActive = false;
  798. let autoRunCurrentRun = 0;
  799. let autoRunTotalRuns = 1;
  800. // Outer loop: runs the full flow N times
  801. async function autoRunLoop(totalRuns) {
  802. if (autoRunActive) {
  803. await addLog('Auto run already in progress', 'warn');
  804. return;
  805. }
  806. clearStopRequest();
  807. autoRunActive = true;
  808. autoRunTotalRuns = totalRuns;
  809. let successCount = 0;
  810. let failCount = 0;
  811. await setState({ autoRunning: true });
  812. for (let run = 1; run <= totalRuns; run++) {
  813. autoRunCurrentRun = run;
  814. // Reset everything at the start of each run (keep VPS/mail settings)
  815. const prevState = await getState();
  816. await resetState();
  817. await setState({ autoRunning: true });
  818. // Tell side panel to reset all UI
  819. chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
  820. await sleepWithStop(500);
  821. await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
  822. const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns, successCount } });
  823. try {
  824. throwIfStopped();
  825. chrome.runtime.sendMessage(status('running')).catch(() => {});
  826. await executeStepAndWait(1, 2000);
  827. await executeStepAndWait(2, 2000);
  828. const generatedEmail = await generateYibasiEmail();
  829. await addLog(`=== Run ${run}/${totalRuns} — Email ready: ${generatedEmail} ===`, 'ok');
  830. await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
  831. chrome.runtime.sendMessage(status('running')).catch(() => {});
  832. const signupTabId = await getTabId('signup-page');
  833. if (signupTabId) {
  834. await chrome.tabs.update(signupTabId, { active: true });
  835. }
  836. await executeStepAndWait(3, 3000);
  837. await executeStepAndWait(4, 2000);
  838. await executeStepAndWait(5, 3000);
  839. await executeStepAndWait(6, 3000);
  840. await executeStepAndWait(7, 2000);
  841. // After login+verification, check if page landed on add-phone (only expected after step 5, not here)
  842. const phoneCheckTabId = await getTabId('signup-page');
  843. if (phoneCheckTabId) {
  844. try {
  845. const phoneTab = await chrome.tabs.get(phoneCheckTabId);
  846. if (phoneTab.url && (phoneTab.url.includes('add-phone') || phoneTab.url.includes('/phone'))) {
  847. throw new Error('PHONE_VERIFY_REQUIRED');
  848. }
  849. } catch (e) {
  850. if (e.message === 'PHONE_VERIFY_REQUIRED') throw e;
  851. }
  852. }
  853. await executeStepAndWait(8, 2000);
  854. await executeStepAndWait(9, 1000);
  855. successCount++;
  856. await addLog(`=== Run ${run}/${totalRuns} COMPLETE! (${successCount} succeeded, ${failCount} failed) ===`, 'ok');
  857. } catch (err) {
  858. if (isStopError(err)) {
  859. await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
  860. chrome.runtime.sendMessage(status('stopped')).catch(() => {});
  861. break; // Only stop on explicit user stop
  862. } else {
  863. failCount++;
  864. if (err.message === 'PHONE_VERIFY_REQUIRED') {
  865. await addLog(`Run ${run}/${totalRuns} failed: Phone verification required — sleeping 30s before next run (${successCount} succeeded, ${failCount} failed)`, 'error');
  866. await sleepWithStop(30000);
  867. } else {
  868. await addLog(`Run ${run}/${totalRuns} failed: ${err.message} (${successCount} succeeded, ${failCount} failed)`, 'error');
  869. }
  870. await addLog(`Skipping to next run...`, 'warn');
  871. continue;
  872. }
  873. }
  874. }
  875. const attempted = autoRunCurrentRun;
  876. if (stopRequested) {
  877. await addLog(`=== Stopped after ${Math.max(0, attempted - 1)}/${autoRunTotalRuns} runs — ${successCount} succeeded, ${failCount} failed ===`, 'warn');
  878. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
  879. } else {
  880. await addLog(`=== All ${autoRunTotalRuns} runs finished — ${successCount} succeeded, ${failCount} failed ===`, successCount > 0 ? 'ok' : 'warn');
  881. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
  882. }
  883. autoRunActive = false;
  884. await setState({ autoRunning: false });
  885. clearStopRequest();
  886. }
  887. function waitForResume() {
  888. return new Promise((resolve, reject) => {
  889. throwIfStopped();
  890. resumeWaiter = { resolve, reject };
  891. });
  892. }
  893. async function resumeAutoRun() {
  894. throwIfStopped();
  895. const state = await getState();
  896. if (!state.email) {
  897. await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
  898. return;
  899. }
  900. if (resumeWaiter) {
  901. resumeWaiter.resolve();
  902. resumeWaiter = null;
  903. }
  904. }
  905. // ============================================================
  906. // Step 1: Get OAuth Link (via vps-panel.js)
  907. // ============================================================
  908. async function executeStep1(state) {
  909. if (!state.vpsUrl) {
  910. throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
  911. }
  912. await addLog(`Step 1: Opening VPS panel...`);
  913. await reuseOrCreateTab('vps-panel', state.vpsUrl, {
  914. inject: ['content/utils.js', 'content/vps-panel.js'],
  915. reloadIfSameUrl: true,
  916. });
  917. await sendToContentScript('vps-panel', {
  918. type: 'EXECUTE_STEP',
  919. step: 1,
  920. source: 'background',
  921. payload: {},
  922. });
  923. }
  924. // ============================================================
  925. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  926. // ============================================================
  927. async function executeStep2(state) {
  928. if (!state.oauthUrl) {
  929. throw new Error('No OAuth URL. Complete step 1 first.');
  930. }
  931. await addLog(`Step 2: Opening auth URL...`);
  932. await reuseOrCreateTab('signup-page', state.oauthUrl);
  933. await sendToContentScript('signup-page', {
  934. type: 'EXECUTE_STEP',
  935. step: 2,
  936. source: 'background',
  937. payload: {},
  938. });
  939. }
  940. // ============================================================
  941. // Step 3: Fill Email & Password (via signup-page.js)
  942. // ============================================================
  943. async function executeStep3(state) {
  944. if (!state.email) {
  945. throw new Error('No email address. Paste email in Side Panel first.');
  946. }
  947. const password = state.customPassword || generatePassword();
  948. await setPasswordState(password);
  949. // Save account record
  950. const accounts = state.accounts || [];
  951. accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
  952. await setState({ accounts });
  953. await addLog(
  954. `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
  955. );
  956. await sendToContentScript('signup-page', {
  957. type: 'EXECUTE_STEP',
  958. step: 3,
  959. source: 'background',
  960. payload: { email: state.email, password },
  961. });
  962. }
  963. // ============================================================
  964. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  965. // ============================================================
  966. async function getMailConfig(state) {
  967. const provider = state.mailProvider || 'temp-api';
  968. if (provider === 'temp-api') {
  969. let rawUrl = (state.tempApiUrl || '').trim();
  970. // Fallback: read directly from chrome.storage.local in case session is stale
  971. if (!rawUrl) {
  972. const local = await chrome.storage.local.get('tempApiUrl');
  973. rawUrl = (local.tempApiUrl || '').trim();
  974. }
  975. if (!rawUrl) {
  976. return { error: 'Temp Email API URL is empty. Please paste the full URL with JWT in Side Panel.' };
  977. }
  978. // Extract base origin and JWT from the URL (e.g. https://host/?jwt=xxx)
  979. let apiOrigin, jwt;
  980. try {
  981. const parsed = new URL(rawUrl);
  982. apiOrigin = parsed.origin;
  983. jwt = parsed.searchParams.get('jwt');
  984. } catch {
  985. return { error: 'Temp Email API URL is invalid.' };
  986. }
  987. if (!jwt) {
  988. return { error: 'Temp Email API URL has no jwt parameter.' };
  989. }
  990. const pathKey = (state.tempApiMailsPath || 'admin').toLowerCase();
  991. const mailsPath = pathKey === 'admin' ? '/admin/mails' : '/api/mails';
  992. return {
  993. source: 'temp-api',
  994. apiOrigin,
  995. jwt,
  996. mailsPath,
  997. tempApiAdminAuth: (state.tempApiAdminAuth || '').trim(),
  998. tempApiCustomAuth: (state.tempApiCustomAuth || '').trim(),
  999. tempApiFingerprint: (state.tempApiFingerprint || '').trim(),
  1000. tempApiUserToken: (state.tempApiUserToken || '').trim(),
  1001. label: 'Temp Email API',
  1002. useFetch: true, // signal that this provider uses fetch, not a tab
  1003. };
  1004. }
  1005. if (provider === '163') {
  1006. 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' };
  1007. }
  1008. if (provider === 'gmail') {
  1009. const gmailBaseEmail = (state.gmailBaseEmail || '').trim().toLowerCase();
  1010. const gmailApiClientId = (state.gmailApiClientId || '').trim();
  1011. const gmailApiClientSecret = (state.gmailApiClientSecret || '').trim();
  1012. const gmailApiRefreshToken = (state.gmailApiRefreshToken || '').trim();
  1013. const gmailApiAccessToken = (state.gmailApiAccessToken || '').trim();
  1014. const gmailApiAccessTokenExpiresAt = Number(state.gmailApiAccessTokenExpiresAt || 0);
  1015. if (!gmailBaseEmail || !/^[^@]+@gmail\.com$/i.test(gmailBaseEmail)) {
  1016. return { error: 'Gmail 基础邮箱无效,请在 Side Panel 填写 gmail.com 地址。' };
  1017. }
  1018. const hasRefreshCredentials = gmailApiClientId && gmailApiClientSecret && gmailApiRefreshToken;
  1019. if (!hasRefreshCredentials && !gmailApiAccessToken) {
  1020. return { error: 'Gmail API 凭证不完整。请填写 client_id / client_secret / refresh_token,或手动填写 access token。' };
  1021. }
  1022. return {
  1023. source: 'gmail-api',
  1024. label: `Gmail API (${gmailBaseEmail})`,
  1025. useFetch: true,
  1026. fetchSource: 'gmail-api',
  1027. gmailBaseEmail,
  1028. gmailApiClientId,
  1029. gmailApiClientSecret,
  1030. gmailApiRefreshToken,
  1031. gmailApiAccessToken,
  1032. gmailApiAccessTokenExpiresAt,
  1033. };
  1034. }
  1035. if (provider === 'inbucket') {
  1036. const host = normalizeInbucketOrigin(state.inbucketHost);
  1037. const mailbox = (state.inbucketMailbox || '').trim();
  1038. if (!host) {
  1039. return { error: 'Inbucket host is empty or invalid.' };
  1040. }
  1041. if (!mailbox) {
  1042. return { error: 'Inbucket mailbox name is empty.' };
  1043. }
  1044. return {
  1045. source: 'inbucket-mail',
  1046. url: `${host}/m/${encodeURIComponent(mailbox)}/`,
  1047. label: `Inbucket Mailbox (${mailbox})`,
  1048. navigateOnReuse: true,
  1049. inject: ['content/utils.js', 'content/inbucket-mail.js'],
  1050. injectSource: 'inbucket-mail',
  1051. };
  1052. }
  1053. return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
  1054. }
  1055. function normalizeInbucketOrigin(rawValue) {
  1056. const value = (rawValue || '').trim();
  1057. if (!value) return '';
  1058. const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
  1059. try {
  1060. const parsed = new URL(candidate);
  1061. return parsed.origin;
  1062. } catch {
  1063. return '';
  1064. }
  1065. }
  1066. // ============================================================
  1067. // Temp Email API: fetch-based polling (no tab needed)
  1068. // ============================================================
  1069. function extractVerificationCodeFromText(text) {
  1070. // Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
  1071. const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
  1072. if (matchCn) return matchCn[1];
  1073. // Pattern 2: English format "code is 370794" or "code: 370794"
  1074. const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
  1075. if (matchEn) return matchEn[1] || matchEn[2];
  1076. // Pattern 3: standalone 6-digit number
  1077. const match6 = text.match(/\b(\d{6})\b/);
  1078. if (match6) return match6[1];
  1079. return null;
  1080. }
  1081. function decodeMimeWord(encoded) {
  1082. // Decode =?charset?encoding?text?= (RFC 2047)
  1083. return encoded.replace(/=\?([^?]+)\?(B|Q)\?([^?]*)\?=/gi, (_, charset, enc, text) => {
  1084. if (enc.toUpperCase() === 'B') {
  1085. // Base64
  1086. try { return atob(text); } catch { return text; }
  1087. }
  1088. // Quoted-Printable
  1089. return text.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (__, hex) =>
  1090. String.fromCharCode(parseInt(hex, 16))
  1091. );
  1092. });
  1093. }
  1094. function decodeUtf8Bytes(str) {
  1095. // If the decoded string contains raw UTF-8 bytes, decode them
  1096. try { return decodeURIComponent(escape(str)); } catch { return str; }
  1097. }
  1098. function extractSubjectFromRaw(raw) {
  1099. // Extract Subject header (may span multiple lines with folding)
  1100. const match = raw.match(/^Subject:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
  1101. if (!match) return '';
  1102. // Unfold continuation lines and decode MIME words
  1103. const unfolded = match[1].replace(/\r?\n\s+/g, ' ').trim();
  1104. return decodeUtf8Bytes(decodeMimeWord(unfolded));
  1105. }
  1106. function extractFromHeader(raw) {
  1107. const match = raw.match(/^From:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
  1108. if (!match) return '';
  1109. return match[1].replace(/\r?\n\s+/g, ' ').trim().toLowerCase();
  1110. }
  1111. function extractPlainTextFromRaw(raw) {
  1112. // Try to extract the text/plain part from a multipart email
  1113. const plainMatch = raw.match(/Content-Type:\s*text\/plain[\s\S]*?\r?\n\r?\n([\s\S]*?)(?:\r?\n--|\r?\n$)/i);
  1114. if (plainMatch) {
  1115. let text = plainMatch[1];
  1116. // Decode quoted-printable
  1117. text = text.replace(/=\r?\n/g, '');
  1118. text = text.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
  1119. try { return decodeURIComponent(escape(text)); } catch { return text; }
  1120. }
  1121. return raw;
  1122. }
  1123. /** Parse Temp Email API `created_at` (ISO string or unix sec/ms) to milliseconds; 0 if unknown. */
  1124. function tempApiMailTimeMs(m) {
  1125. const v = m?.created_at;
  1126. if (v == null) return 0;
  1127. if (typeof v === 'number') return v < 1e12 ? v * 1000 : v;
  1128. const s = String(v).trim();
  1129. const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(s)
  1130. ? s.replace(' ', 'T') + 'Z'
  1131. : s;
  1132. const t = Date.parse(normalized);
  1133. return Number.isNaN(t) ? 0 : t;
  1134. }
  1135. function decodeBase64Url(input) {
  1136. const normalized = String(input || '').replace(/-/g, '+').replace(/_/g, '/');
  1137. const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
  1138. try {
  1139. return decodeUtf8Bytes(atob(padded));
  1140. } catch {
  1141. return '';
  1142. }
  1143. }
  1144. function collectGmailMessageParts(part, acc = []) {
  1145. if (!part) return acc;
  1146. acc.push(part);
  1147. if (Array.isArray(part.parts)) {
  1148. for (const child of part.parts) collectGmailMessageParts(child, acc);
  1149. }
  1150. return acc;
  1151. }
  1152. function getHeaderValue(headers, name) {
  1153. const hit = (headers || []).find(h => String(h.name || '').toLowerCase() === name.toLowerCase());
  1154. return hit?.value || '';
  1155. }
  1156. function extractGmailMessageText(message) {
  1157. const payload = message?.payload || {};
  1158. const parts = collectGmailMessageParts(payload);
  1159. const texts = [];
  1160. for (const part of parts) {
  1161. const mimeType = String(part?.mimeType || '').toLowerCase();
  1162. const data = part?.body?.data;
  1163. if (!data) continue;
  1164. if (mimeType.startsWith('text/plain') || mimeType.startsWith('text/html') || mimeType === '') {
  1165. texts.push(decodeBase64Url(data));
  1166. }
  1167. }
  1168. if (!texts.length && payload?.body?.data) {
  1169. texts.push(decodeBase64Url(payload.body.data));
  1170. }
  1171. return texts.join('\n');
  1172. }
  1173. function messageMatchesTargetAlias(message, targetEmail) {
  1174. const target = String(targetEmail || '').trim().toLowerCase();
  1175. if (!target) return true;
  1176. const headers = message?.payload?.headers || [];
  1177. const toHeader = `${getHeaderValue(headers, 'to')} ${getHeaderValue(headers, 'delivered-to')} ${getHeaderValue(headers, 'x-original-to')}`.toLowerCase();
  1178. const bodyText = extractGmailMessageText(message).toLowerCase();
  1179. const snippet = String(message?.snippet || '').toLowerCase();
  1180. return toHeader.includes(target) || bodyText.includes(target) || snippet.includes(target);
  1181. }
  1182. function gmailMessageTimeMs(message) {
  1183. const raw = Number(message?.internalDate || 0);
  1184. return Number.isFinite(raw) ? raw : 0;
  1185. }
  1186. async function fetchGmailApiJson(path, accessToken, params = {}) {
  1187. const url = new URL(`https://gmail.googleapis.com/gmail/v1/users/me/${path}`);
  1188. for (const [key, value] of Object.entries(params)) {
  1189. if (value !== undefined && value !== null && value !== '') {
  1190. url.searchParams.set(key, String(value));
  1191. }
  1192. }
  1193. const resp = await fetch(url.toString(), {
  1194. headers: {
  1195. 'Authorization': `Bearer ${accessToken}`,
  1196. 'Accept': 'application/json',
  1197. },
  1198. });
  1199. if (!resp.ok) {
  1200. let detail = '';
  1201. try {
  1202. const json = await resp.json();
  1203. detail = json?.error?.message || '';
  1204. } catch {}
  1205. throw new Error(`Gmail API returned ${resp.status}${detail ? `: ${detail}` : ''}`);
  1206. }
  1207. return resp.json();
  1208. }
  1209. async function refreshGmailApiAccessToken(mail) {
  1210. const body = new URLSearchParams({
  1211. client_id: mail.gmailApiClientId,
  1212. client_secret: mail.gmailApiClientSecret,
  1213. refresh_token: mail.gmailApiRefreshToken,
  1214. grant_type: 'refresh_token',
  1215. });
  1216. const resp = await fetch('https://oauth2.googleapis.com/token', {
  1217. method: 'POST',
  1218. headers: {
  1219. 'Content-Type': 'application/x-www-form-urlencoded',
  1220. 'Accept': 'application/json',
  1221. },
  1222. body: body.toString(),
  1223. });
  1224. const json = await resp.json().catch(() => ({}));
  1225. if (!resp.ok || !json?.access_token) {
  1226. const detail = json?.error_description || json?.error || `HTTP ${resp.status}`;
  1227. throw new Error(`Gmail token refresh failed: ${detail}`);
  1228. }
  1229. const expiresInSec = Number(json.expires_in || 3600);
  1230. const expiresAt = Date.now() + Math.max(60, expiresInSec - 60) * 1000;
  1231. await setState({
  1232. gmailApiAccessToken: json.access_token,
  1233. gmailApiAccessTokenExpiresAt: expiresAt,
  1234. });
  1235. return json.access_token;
  1236. }
  1237. async function getValidGmailApiAccessToken(mail) {
  1238. const hasRefreshCredentials = mail.gmailApiClientId && mail.gmailApiClientSecret && mail.gmailApiRefreshToken;
  1239. if (hasRefreshCredentials) {
  1240. const expiresAt = Number(mail.gmailApiAccessTokenExpiresAt || 0);
  1241. if (mail.gmailApiAccessToken && expiresAt > Date.now()) {
  1242. return mail.gmailApiAccessToken;
  1243. }
  1244. return refreshGmailApiAccessToken(mail);
  1245. }
  1246. if (!mail.gmailApiAccessToken) {
  1247. throw new Error('Gmail API access token is empty.');
  1248. }
  1249. return mail.gmailApiAccessToken;
  1250. }
  1251. async function pollGmailApi(mail, step, payload) {
  1252. const {
  1253. senderFilters,
  1254. subjectFilters,
  1255. maxAttempts,
  1256. intervalMs,
  1257. usedMailIds,
  1258. usedCodes,
  1259. filterAfterTimestamp = 0,
  1260. targetEmail = '',
  1261. } = payload;
  1262. const usedIdSet = new Set((usedMailIds || []).map(String));
  1263. const usedCodeSet = new Set(usedCodes || []);
  1264. const targetLower = String(targetEmail || '').trim().toLowerCase();
  1265. const gmailQuery = [targetLower ? `to:${targetLower}` : '', 'newer_than:2d'].filter(Boolean).join(' ');
  1266. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  1267. throwIfStopped();
  1268. await addLog(`Step ${step}: Polling Gmail API... attempt ${attempt}/${maxAttempts}`);
  1269. try {
  1270. const accessToken = await getValidGmailApiAccessToken(mail);
  1271. const list = await fetchGmailApiJson('messages', accessToken, {
  1272. includeSpamTrash: 'false',
  1273. labelIds: 'INBOX',
  1274. maxResults: '15',
  1275. q: gmailQuery,
  1276. });
  1277. const messages = Array.isArray(list?.messages) ? list.messages : [];
  1278. await addLog(`Step ${step}: Gmail API returned ${messages.length} message refs`, 'info');
  1279. for (const ref of messages) {
  1280. if (!ref?.id || usedIdSet.has(String(ref.id))) continue;
  1281. const message = await fetchGmailApiJson(`messages/${encodeURIComponent(ref.id)}`, accessToken, {
  1282. format: 'full',
  1283. });
  1284. const messageTime = gmailMessageTimeMs(message);
  1285. if (filterAfterTimestamp > 0 && messageTime > 0 && messageTime < filterAfterTimestamp - 60000) {
  1286. continue;
  1287. }
  1288. const headers = message?.payload?.headers || [];
  1289. const subject = decodeUtf8Bytes(decodeMimeWord(getHeaderValue(headers, 'subject'))).toLowerCase();
  1290. const fromHeader = getHeaderValue(headers, 'from').toLowerCase();
  1291. const messageText = extractGmailMessageText(message);
  1292. const snippet = String(message?.snippet || '');
  1293. const fullText = `${subject}\n${snippet}\n${messageText}`;
  1294. if (!messageMatchesTargetAlias(message, targetLower)) {
  1295. await addLog(`Step ${step}: Gmail API skipped message ${ref.id} because alias did not match ${targetLower}`, 'info');
  1296. continue;
  1297. }
  1298. const senderMatch = senderFilters.some(f => fromHeader.includes(String(f).toLowerCase()));
  1299. const subjectMatch = subjectFilters.some(f => subject.includes(String(f).toLowerCase()));
  1300. const code = extractVerificationCodeFromText(fullText);
  1301. if (code && (senderMatch || subjectMatch || fromHeader.includes('openai'))) {
  1302. if (usedCodeSet.has(code)) {
  1303. await addLog(`Step ${step}: Gmail API skipping already-used code ${code} (message ${ref.id})`, 'info');
  1304. continue;
  1305. }
  1306. await addLog(`Step ${step}: Code found from Gmail API: ${code} (subject: ${subject.slice(0, 40)})`, 'ok');
  1307. return {
  1308. ok: true,
  1309. code,
  1310. emailTimestamp: messageTime > 0 ? messageTime : Date.now(),
  1311. mailId: String(ref.id),
  1312. };
  1313. }
  1314. }
  1315. } catch (err) {
  1316. await addLog(`Step ${step}: Gmail API fetch error: ${err.message}`, 'warn');
  1317. }
  1318. if (attempt < maxAttempts) {
  1319. await sleepWithStop(intervalMs);
  1320. }
  1321. }
  1322. throw new Error(
  1323. `No matching email found via Gmail API after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
  1324. 'Check token scope, alias, or mailbox filters.'
  1325. );
  1326. }
  1327. async function pollTempEmailApi(mail, step, payload) {
  1328. const {
  1329. senderFilters,
  1330. subjectFilters,
  1331. maxAttempts,
  1332. intervalMs,
  1333. usedMailIds,
  1334. usedCodes,
  1335. filterAfterTimestamp = 0,
  1336. targetEmail = '',
  1337. } = payload;
  1338. const usedIdSet = new Set((usedMailIds || []).map(String));
  1339. const usedCodeSet = new Set(usedCodes || []);
  1340. const targetLower = (targetEmail || '').trim().toLowerCase();
  1341. const mailsPath = mail.mailsPath || '/api/mails';
  1342. const headers = {
  1343. 'Authorization': `Bearer ${mail.jwt}`,
  1344. 'Accept': 'application/json, text/plain, */*',
  1345. };
  1346. if (mailsPath === '/admin/mails') {
  1347. headers['x-lang'] = 'zh';
  1348. if (mail.tempApiAdminAuth) headers['x-admin-auth'] = mail.tempApiAdminAuth;
  1349. if (mail.tempApiCustomAuth) headers['x-custom-auth'] = mail.tempApiCustomAuth;
  1350. if (mail.tempApiFingerprint) headers['x-fingerprint'] = mail.tempApiFingerprint;
  1351. if (mail.tempApiUserToken) headers['x-user-token'] = mail.tempApiUserToken;
  1352. }
  1353. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  1354. throwIfStopped();
  1355. await addLog(`Step ${step}: Polling Temp Email API... attempt ${attempt}/${maxAttempts}`);
  1356. try {
  1357. const params = new URLSearchParams({
  1358. limit: '30',
  1359. offset: '0',
  1360. });
  1361. if (targetLower) {
  1362. params.set('address', targetLower);
  1363. }
  1364. const resp = await fetch(`${mail.apiOrigin}${mailsPath}?${params.toString()}`, { headers });
  1365. if (!resp.ok) throw new Error(`API returned ${resp.status}`);
  1366. const data = await resp.json();
  1367. const mailResults = Array.isArray(data)
  1368. ? data
  1369. : Array.isArray(data?.results)
  1370. ? data.results
  1371. : Array.isArray(data?.data)
  1372. ? data.data
  1373. : Array.isArray(data?.items)
  1374. ? data.items
  1375. : [];
  1376. const mails = mailResults.slice().sort((a, b) => tempApiMailTimeMs(b) - tempApiMailTimeMs(a));
  1377. await addLog(`Step ${step}: Temp API returned ${mails.length} mails`, 'info');
  1378. for (const m of mails) {
  1379. if (usedIdSet.has(String(m.id))) {
  1380. await addLog(`Step ${step}: Skipping used mail id ${m.id}`, 'info');
  1381. continue;
  1382. }
  1383. const mailTime = tempApiMailTimeMs(m);
  1384. if (filterAfterTimestamp > 0 && mailTime > 0 && mailTime < filterAfterTimestamp - 60000) {
  1385. await addLog(
  1386. `Step ${step}: Skipping old mail id ${m.id} (mail: ${new Date(mailTime).toLocaleString()}, after: ${new Date(filterAfterTimestamp).toLocaleString()})`,
  1387. 'info'
  1388. );
  1389. continue;
  1390. }
  1391. const raw = String(m.raw || m.raw_text || m.text || m.body || m.content || '');
  1392. const html = String(m.html || m.raw_html || '');
  1393. const fullText = `${raw}\n${html}`;
  1394. const mailAddress = String(m.address || m.mailbox || m.email || '').trim().toLowerCase();
  1395. const targetMatched = !targetLower
  1396. || mailAddress === targetLower
  1397. || fullText.toLowerCase().includes(targetLower);
  1398. const source = (m.source || '').toLowerCase();
  1399. const fromHeader = extractFromHeader(fullText);
  1400. const subject = extractSubjectFromRaw(raw).toLowerCase();
  1401. const plainText = extractPlainTextFromRaw(fullText);
  1402. // Match against both envelope sender and From header
  1403. const senderMatch = senderFilters.some(f => {
  1404. const fl = f.toLowerCase();
  1405. return source.includes(fl) || fromHeader.includes(fl);
  1406. });
  1407. const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
  1408. const code = extractVerificationCodeFromText(subject + ' ' + plainText);
  1409. const keywordCodeMatch = Boolean(code) && (senderMatch || subjectMatch);
  1410. const targetOnlyCodeMatch = Boolean(code) && targetMatched;
  1411. if (keywordCodeMatch || targetOnlyCodeMatch) {
  1412. if (code) {
  1413. if (usedCodeSet.has(code)) {
  1414. console.log(LOG_PREFIX, `Step ${step}: Skipping already-used code ${code} (id: ${m.id})`);
  1415. continue;
  1416. }
  1417. await addLog(
  1418. `Step ${step}: Code found from API: ${code} (subject: ${subject.slice(0, 40)}, targetMatched: ${targetMatched ? 'yes' : 'no'})`,
  1419. 'ok'
  1420. );
  1421. const emailTimestamp = mailTime > 0 ? mailTime : Date.now();
  1422. return { ok: true, code, emailTimestamp, mailId: String(m.id) };
  1423. }
  1424. }
  1425. await addLog(
  1426. `Step ${step}: Mail ${m.id} not matched (address: ${mailAddress || 'n/a'}, senderMatch: ${senderMatch ? 'yes' : 'no'}, subjectMatch: ${subjectMatch ? 'yes' : 'no'}, targetMatched: ${targetMatched ? 'yes' : 'no'}, code: ${code || 'none'})`,
  1427. 'info'
  1428. );
  1429. }
  1430. } catch (err) {
  1431. await addLog(`Step ${step}: API fetch error: ${err.message}`, 'warn');
  1432. }
  1433. if (attempt < maxAttempts) {
  1434. await sleepWithStop(intervalMs);
  1435. }
  1436. }
  1437. throw new Error(
  1438. `No matching email found via Temp Email API after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
  1439. 'Email may be delayed.'
  1440. );
  1441. }
  1442. async function clickResendOnSignupPage(step) {
  1443. const signupTabId = await getTabId('signup-page');
  1444. if (!signupTabId) return;
  1445. await chrome.tabs.update(signupTabId, { active: true });
  1446. await sleepWithStop(500);
  1447. try {
  1448. await sendToContentScript('signup-page', {
  1449. type: 'CLICK_RESEND_EMAIL',
  1450. step,
  1451. source: 'background',
  1452. });
  1453. } catch (err) {
  1454. await addLog(`Step ${step}: Resend click skipped: ${err.message}`, 'warn');
  1455. }
  1456. }
  1457. async function executeStep4(state) {
  1458. // Capture poll baseline before any resend wait, with a safety buffer for emails
  1459. // that arrive immediately after form submission but before polling begins.
  1460. const verificationPollSince = Date.now() - 120000;
  1461. // Re-read state to ensure tempApiUrl is fresh (may have been saved after initial getState)
  1462. const freshState = await getState();
  1463. const mail = await getMailConfig(freshState);
  1464. if (mail.error) throw new Error(mail.error);
  1465. const pollPayload = {
  1466. filterAfterTimestamp: verificationPollSince,
  1467. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  1468. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
  1469. targetEmail: freshState.email,
  1470. maxAttempts: 20,
  1471. intervalMs: 3000,
  1472. usedMailIds: freshState.usedMailIds || [],
  1473. usedCodes: freshState.usedCodes || [],
  1474. };
  1475. let result;
  1476. if (mail.useFetch) {
  1477. // API-based providers: fetch directly, no tab needed
  1478. await addLog(`Step 4: Polling ${mail.label}...`);
  1479. result = mail.fetchSource === 'gmail-api'
  1480. ? await pollGmailApi(mail, 4, pollPayload)
  1481. : await pollTempEmailApi(mail, 4, pollPayload);
  1482. } else {
  1483. await addLog(`Step 4: Opening ${mail.label}...`);
  1484. // For mail tabs, only create if not alive — don't navigate (preserves login session)
  1485. const alive = await isTabAlive(mail.source);
  1486. if (alive) {
  1487. if (mail.navigateOnReuse) {
  1488. await reuseOrCreateTab(mail.source, mail.url, {
  1489. inject: mail.inject,
  1490. injectSource: mail.injectSource,
  1491. });
  1492. } else {
  1493. const tabId = await getTabId(mail.source);
  1494. await chrome.tabs.update(tabId, { active: true });
  1495. }
  1496. } else {
  1497. await reuseOrCreateTab(mail.source, mail.url, {
  1498. inject: mail.inject,
  1499. injectSource: mail.injectSource,
  1500. });
  1501. }
  1502. result = await sendToContentScript(mail.source, {
  1503. type: 'POLL_EMAIL',
  1504. step: 4,
  1505. source: 'background',
  1506. payload: pollPayload,
  1507. });
  1508. }
  1509. if (result && result.error) {
  1510. throw new Error(result.error);
  1511. }
  1512. if (result && result.code) {
  1513. // Track used mail IDs and codes to avoid reusing stale ones
  1514. const latest = await getState();
  1515. const usedMailIds = [...(latest.usedMailIds || [])];
  1516. const usedCodes = [...(latest.usedCodes || [])];
  1517. if (result.mailId) usedMailIds.push(result.mailId);
  1518. usedCodes.push(result.code);
  1519. await setState({ lastEmailTimestamp: result.emailTimestamp, usedMailIds, usedCodes });
  1520. await addLog(`Step 4: Got verification code: ${result.code}`);
  1521. // Switch to signup tab and fill code
  1522. const signupTabId = await getTabId('signup-page');
  1523. if (signupTabId) {
  1524. await addLog('Step 4: Switching back to signup page to fill verification code...', 'info');
  1525. await chrome.tabs.update(signupTabId, { active: true });
  1526. const ready = await waitForContentScriptReady('signup-page', 8000);
  1527. if (!ready) {
  1528. await addLog('Step 4: signup-page content script not marked ready yet; sending code anyway...', 'warn');
  1529. }
  1530. await addLog('Step 4: Sending verification code to signup page...', 'info');
  1531. await sendToContentScript('signup-page', {
  1532. type: 'FILL_CODE',
  1533. step: 4,
  1534. source: 'background',
  1535. payload: { code: result.code },
  1536. });
  1537. } else {
  1538. throw new Error('Signup page tab was closed. Cannot fill verification code.');
  1539. }
  1540. }
  1541. }
  1542. // ============================================================
  1543. // Step 5: Fill Name & Birthday (via signup-page.js)
  1544. // ============================================================
  1545. async function executeStep5(state) {
  1546. const { firstName, lastName } = generateRandomName();
  1547. const { year, month, day } = generateRandomBirthday();
  1548. // Save birthday data to state for potential reuse on about-you page
  1549. await setState({ birthYear: year, birthMonth: month, birthDay: day, fullName: `${firstName} ${lastName}` });
  1550. await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
  1551. await sendToContentScript('signup-page', {
  1552. type: 'EXECUTE_STEP',
  1553. step: 5,
  1554. source: 'background',
  1555. payload: { firstName, lastName, year, month, day },
  1556. });
  1557. }
  1558. // ============================================================
  1559. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  1560. // ============================================================
  1561. async function executeStep6(state) {
  1562. if (!state.oauthUrl) {
  1563. throw new Error('No OAuth URL. Complete step 1 first.');
  1564. }
  1565. if (!state.email) {
  1566. throw new Error('No email. Complete step 3 first.');
  1567. }
  1568. await addLog(`Step 6: Opening OAuth URL for login...`);
  1569. // Reuse the signup-page tab — navigate it to the OAuth URL
  1570. await reuseOrCreateTab('signup-page', state.oauthUrl);
  1571. // signup-page.js will inject (same auth.openai.com domain) and handle login
  1572. await sendToContentScript('signup-page', {
  1573. type: 'EXECUTE_STEP',
  1574. step: 6,
  1575. source: 'background',
  1576. payload: { email: state.email, password: state.password },
  1577. });
  1578. }
  1579. // ============================================================
  1580. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  1581. // ============================================================
  1582. async function executeStep7(state) {
  1583. // Check if the page is on /about-you (birthday not filled during signup)
  1584. // and handle it before proceeding with verification code polling
  1585. try {
  1586. const aboutYouResult = await sendToContentScript('signup-page', {
  1587. type: 'HANDLE_ABOUT_YOU',
  1588. step: 7,
  1589. source: 'background',
  1590. payload: {
  1591. year: state.birthYear,
  1592. month: state.birthMonth,
  1593. day: state.birthDay,
  1594. fullName: state.fullName,
  1595. },
  1596. });
  1597. if (aboutYouResult && aboutYouResult.handled) {
  1598. await addLog('Step 7: Handled about-you page, waiting for next page...', 'info');
  1599. await sleep(3000);
  1600. }
  1601. } catch (e) {
  1602. // Not on about-you page, continue normally
  1603. }
  1604. // Capture poll baseline before any resend wait, with a safety buffer for emails
  1605. // that may already be in flight when the auth page is reached.
  1606. const verificationPollSince = Date.now() - 120000;
  1607. const mail = await getMailConfig(await getState());
  1608. if (mail.error) throw new Error(mail.error);
  1609. // Re-read state to get latest usedMailIds/usedCodes
  1610. const freshState = await getState();
  1611. const pollPayload = {
  1612. filterAfterTimestamp: verificationPollSince,
  1613. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  1614. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  1615. targetEmail: freshState.email,
  1616. maxAttempts: 20,
  1617. intervalMs: 3000,
  1618. usedMailIds: freshState.usedMailIds || [],
  1619. usedCodes: freshState.usedCodes || [],
  1620. };
  1621. let result;
  1622. if (mail.useFetch) {
  1623. await addLog(`Step 7: Polling ${mail.label}...`);
  1624. result = mail.fetchSource === 'gmail-api'
  1625. ? await pollGmailApi(mail, 7, pollPayload)
  1626. : await pollTempEmailApi(mail, 7, pollPayload);
  1627. } else {
  1628. await addLog(`Step 7: Opening ${mail.label}...`);
  1629. const alive = await isTabAlive(mail.source);
  1630. if (alive) {
  1631. if (mail.navigateOnReuse) {
  1632. await reuseOrCreateTab(mail.source, mail.url, {
  1633. inject: mail.inject,
  1634. injectSource: mail.injectSource,
  1635. });
  1636. } else {
  1637. const tabId = await getTabId(mail.source);
  1638. await chrome.tabs.update(tabId, { active: true });
  1639. }
  1640. } else {
  1641. await reuseOrCreateTab(mail.source, mail.url, {
  1642. inject: mail.inject,
  1643. injectSource: mail.injectSource,
  1644. });
  1645. }
  1646. result = await sendToContentScript(mail.source, {
  1647. type: 'POLL_EMAIL',
  1648. step: 7,
  1649. source: 'background',
  1650. payload: pollPayload,
  1651. });
  1652. }
  1653. if (result && result.error) {
  1654. throw new Error(result.error);
  1655. }
  1656. if (result && result.code) {
  1657. // Track used mail IDs and codes
  1658. const latest = await getState();
  1659. const usedMailIds = [...(latest.usedMailIds || [])];
  1660. const usedCodes = [...(latest.usedCodes || [])];
  1661. if (result.mailId) usedMailIds.push(result.mailId);
  1662. usedCodes.push(result.code);
  1663. await setState({ lastEmailTimestamp: result.emailTimestamp, usedMailIds, usedCodes });
  1664. await addLog(`Step 7: Got login verification code: ${result.code}`);
  1665. // Switch to signup/auth tab and fill code
  1666. const signupTabId = await getTabId('signup-page');
  1667. if (signupTabId) {
  1668. await addLog('Step 7: Switching back to auth page to fill verification code...', 'info');
  1669. await chrome.tabs.update(signupTabId, { active: true });
  1670. const ready = await waitForContentScriptReady('signup-page', 8000);
  1671. if (!ready) {
  1672. await addLog('Step 7: signup-page content script not marked ready yet; sending code anyway...', 'warn');
  1673. }
  1674. await addLog('Step 7: Sending verification code to auth page...', 'info');
  1675. await sendToContentScript('signup-page', {
  1676. type: 'FILL_CODE',
  1677. step: 7,
  1678. source: 'background',
  1679. payload: { code: result.code },
  1680. });
  1681. } else {
  1682. throw new Error('Auth page tab was closed. Cannot fill verification code.');
  1683. }
  1684. }
  1685. }
  1686. // ============================================================
  1687. // Step 8: Complete OAuth (auto click + localhost listener)
  1688. // ============================================================
  1689. let webNavListener = null;
  1690. async function executeStep8(state) {
  1691. if (!state.oauthUrl) {
  1692. throw new Error('No OAuth URL. Complete step 1 first.');
  1693. }
  1694. // Check if the signup tab already redirected to localhost before listener setup
  1695. const signupTabIdEarly = await getTabId('signup-page');
  1696. if (signupTabIdEarly) {
  1697. try {
  1698. const tab = await chrome.tabs.get(signupTabIdEarly);
  1699. if (tab.url && (tab.url.startsWith('http://localhost') || tab.url.startsWith('http://127.0.0.1'))) {
  1700. await addLog(`Step 8: Localhost redirect already captured: ${tab.url}`, 'ok');
  1701. await setState({ localhostUrl: tab.url });
  1702. broadcastDataUpdate({ localhostUrl: tab.url });
  1703. return;
  1704. }
  1705. } catch {}
  1706. }
  1707. await addLog('Step 8: Setting up localhost redirect listener...');
  1708. // Register webNavigation listener (scoped to this step)
  1709. return new Promise((resolve, reject) => {
  1710. let resolved = false;
  1711. const isLocalhostUrl = (url) =>
  1712. url && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1'));
  1713. const cleanupListeners = () => {
  1714. if (webNavListener) {
  1715. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  1716. chrome.webNavigation.onCommitted.removeListener(webNavListener);
  1717. chrome.webNavigation.onErrorOccurred.removeListener(webNavListener);
  1718. webNavListener = null;
  1719. }
  1720. };
  1721. const captureLocalhostUrl = (url) => {
  1722. if (resolved) return;
  1723. resolved = true;
  1724. cleanupListeners();
  1725. clearTimeout(timeout);
  1726. setState({ localhostUrl: url }).then(() => {
  1727. addLog(`Step 8: Captured localhost URL: ${url}`, 'ok');
  1728. setStepStatus(8, 'completed');
  1729. notifyStepComplete(8, { localhostUrl: url });
  1730. broadcastDataUpdate({ localhostUrl: url });
  1731. resolve();
  1732. });
  1733. };
  1734. const timeout = setTimeout(() => {
  1735. cleanupListeners();
  1736. reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
  1737. }, 120000);
  1738. webNavListener = (details) => {
  1739. if (details.frameId === 0 && isLocalhostUrl(details.url)) {
  1740. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  1741. captureLocalhostUrl(details.url);
  1742. }
  1743. };
  1744. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  1745. chrome.webNavigation.onCommitted.addListener(webNavListener);
  1746. chrome.webNavigation.onErrorOccurred.addListener(webNavListener);
  1747. // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
  1748. // with a "继续" button. Content script clicks it directly (no debugger needed).
  1749. (async () => {
  1750. try {
  1751. let signupTabId = await getTabId('signup-page');
  1752. if (signupTabId) {
  1753. await chrome.tabs.update(signupTabId, { active: true });
  1754. await addLog('Step 8: Switched to auth page...');
  1755. } else {
  1756. signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
  1757. await addLog('Step 8: Auth tab reopened...');
  1758. }
  1759. const clickResult = await sendToContentScript('signup-page', {
  1760. type: 'STEP8_FIND_AND_CLICK',
  1761. source: 'background',
  1762. payload: {},
  1763. });
  1764. if (clickResult?.error) {
  1765. throw new Error(clickResult.error);
  1766. }
  1767. await addLog('Step 8: "继续" button clicked, waiting for redirect...');
  1768. // Poll tab URL in case webNavigation listeners missed the redirect
  1769. for (let i = 0; i < 30 && !resolved; i++) {
  1770. await new Promise(r => setTimeout(r, 1000));
  1771. try {
  1772. const tab = await chrome.tabs.get(signupTabId);
  1773. if (isLocalhostUrl(tab.url)) {
  1774. captureLocalhostUrl(tab.url);
  1775. break;
  1776. }
  1777. } catch { break; }
  1778. }
  1779. } catch (err) {
  1780. clearTimeout(timeout);
  1781. cleanupListeners();
  1782. reject(err);
  1783. }
  1784. })();
  1785. });
  1786. }
  1787. // ============================================================
  1788. // Step 9: VPS Verify (via vps-panel.js)
  1789. // ============================================================
  1790. async function executeStep9(state) {
  1791. if (!state.localhostUrl) {
  1792. throw new Error('No localhost URL. Complete step 8 first.');
  1793. }
  1794. if (!state.vpsUrl) {
  1795. throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
  1796. }
  1797. await addLog('Step 9: Opening VPS panel...');
  1798. let tabId = await getTabId('vps-panel');
  1799. const alive = tabId && await isTabAlive('vps-panel');
  1800. if (!alive) {
  1801. // Create new tab in the automation window
  1802. const wid = await ensureAutomationWindowId();
  1803. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true, windowId: wid });
  1804. tabId = tab.id;
  1805. await new Promise(resolve => {
  1806. const listener = (tid, info) => {
  1807. if (tid === tabId && info.status === 'complete') {
  1808. chrome.tabs.onUpdated.removeListener(listener);
  1809. resolve();
  1810. }
  1811. };
  1812. chrome.tabs.onUpdated.addListener(listener);
  1813. });
  1814. } else {
  1815. await chrome.tabs.update(tabId, { active: true });
  1816. }
  1817. // Inject scripts directly and wait for them to be ready
  1818. await chrome.scripting.executeScript({
  1819. target: { tabId },
  1820. files: ['content/utils.js', 'content/vps-panel.js'],
  1821. });
  1822. await new Promise(r => setTimeout(r, 1000));
  1823. // Send command directly — bypass queue/ready mechanism
  1824. await addLog(`Step 9: Filling callback URL...`);
  1825. await chrome.tabs.sendMessage(tabId, {
  1826. type: 'EXECUTE_STEP',
  1827. step: 9,
  1828. source: 'background',
  1829. payload: { localhostUrl: state.localhostUrl },
  1830. });
  1831. }
  1832. // ============================================================
  1833. // Open Side Panel on extension icon click
  1834. // ============================================================
  1835. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });