background.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. // background.js — Service Worker: orchestration, state, tab management, message routing
  2. importScripts('data/names.js');
  3. const LOG_PREFIX = '[MultiPage:bg]';
  4. // ============================================================
  5. // State Management (chrome.storage.session)
  6. // ============================================================
  7. const DEFAULT_STATE = {
  8. currentStep: 0,
  9. stepStatuses: {
  10. 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending',
  11. 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
  12. },
  13. oauthUrl: null,
  14. email: null,
  15. password: 'mimashisha0.0',
  16. lastEmailTimestamp: null,
  17. localhostUrl: null,
  18. flowStartTime: null,
  19. tabRegistry: {},
  20. logs: [],
  21. };
  22. async function getState() {
  23. const state = await chrome.storage.session.get(null);
  24. return { ...DEFAULT_STATE, ...state };
  25. }
  26. async function setState(updates) {
  27. console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
  28. await chrome.storage.session.set(updates);
  29. }
  30. async function resetState() {
  31. console.log(LOG_PREFIX, 'Resetting all state');
  32. await chrome.storage.session.clear();
  33. await chrome.storage.session.set({ ...DEFAULT_STATE });
  34. }
  35. // ============================================================
  36. // Tab Registry
  37. // ============================================================
  38. async function getTabRegistry() {
  39. const state = await getState();
  40. return state.tabRegistry || {};
  41. }
  42. async function registerTab(source, tabId) {
  43. const registry = await getTabRegistry();
  44. registry[source] = { tabId, ready: true };
  45. await setState({ tabRegistry: registry });
  46. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  47. }
  48. async function isTabAlive(source) {
  49. const registry = await getTabRegistry();
  50. const entry = registry[source];
  51. if (!entry) return false;
  52. try {
  53. await chrome.tabs.get(entry.tabId);
  54. return true;
  55. } catch {
  56. // Tab no longer exists — clean up registry
  57. registry[source] = null;
  58. await setState({ tabRegistry: registry });
  59. return false;
  60. }
  61. }
  62. async function getTabId(source) {
  63. const registry = await getTabRegistry();
  64. return registry[source]?.tabId || null;
  65. }
  66. // ============================================================
  67. // Command Queue (for content scripts not yet ready)
  68. // ============================================================
  69. const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
  70. function queueCommand(source, message, timeout = 15000) {
  71. return new Promise((resolve, reject) => {
  72. const timer = setTimeout(() => {
  73. pendingCommands.delete(source);
  74. const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
  75. console.error(LOG_PREFIX, err);
  76. reject(new Error(err));
  77. }, timeout);
  78. pendingCommands.set(source, { message, resolve, reject, timer });
  79. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  80. });
  81. }
  82. function flushCommand(source, tabId) {
  83. const pending = pendingCommands.get(source);
  84. if (pending) {
  85. clearTimeout(pending.timer);
  86. pendingCommands.delete(source);
  87. chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
  88. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  89. }
  90. }
  91. // ============================================================
  92. // Send command to content script (with readiness check)
  93. // ============================================================
  94. async function sendToContentScript(source, message) {
  95. const registry = await getTabRegistry();
  96. const entry = registry[source];
  97. if (!entry || !entry.ready) {
  98. console.log(LOG_PREFIX, `${source} not ready, queuing command`);
  99. return queueCommand(source, message);
  100. }
  101. // Verify tab is still alive
  102. const alive = await isTabAlive(source);
  103. if (!alive) {
  104. // Tab was closed — queue the command, it will be sent when tab is reopened
  105. console.log(LOG_PREFIX, `${source} tab was closed, queuing command`);
  106. return queueCommand(source, message);
  107. }
  108. console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
  109. return chrome.tabs.sendMessage(entry.tabId, message);
  110. }
  111. // ============================================================
  112. // Logging
  113. // ============================================================
  114. async function addLog(message, level = 'info') {
  115. const state = await getState();
  116. const logs = state.logs || [];
  117. const entry = { message, level, timestamp: Date.now() };
  118. logs.push(entry);
  119. // Keep last 500 logs
  120. if (logs.length > 500) logs.splice(0, logs.length - 500);
  121. await setState({ logs });
  122. // Broadcast to side panel
  123. chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
  124. }
  125. // ============================================================
  126. // Step Status Management
  127. // ============================================================
  128. async function setStepStatus(step, status) {
  129. const state = await getState();
  130. const statuses = { ...state.stepStatuses };
  131. statuses[step] = status;
  132. await setState({ stepStatuses: statuses, currentStep: step });
  133. // Broadcast to side panel
  134. chrome.runtime.sendMessage({
  135. type: 'STEP_STATUS_CHANGED',
  136. payload: { step, status },
  137. }).catch(() => {});
  138. }
  139. // ============================================================
  140. // Message Handler (central router)
  141. // ============================================================
  142. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  143. console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
  144. handleMessage(message, sender).then(response => {
  145. sendResponse(response);
  146. }).catch(err => {
  147. console.error(LOG_PREFIX, 'Handler error:', err);
  148. sendResponse({ error: err.message });
  149. });
  150. return true; // async response
  151. });
  152. async function handleMessage(message, sender) {
  153. switch (message.type) {
  154. case 'CONTENT_SCRIPT_READY': {
  155. const tabId = sender.tab?.id;
  156. if (tabId && message.source) {
  157. await registerTab(message.source, tabId);
  158. flushCommand(message.source, tabId);
  159. await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
  160. }
  161. return { ok: true };
  162. }
  163. case 'LOG': {
  164. const { message: msg, level } = message.payload;
  165. await addLog(`[${message.source}] ${msg}`, level);
  166. return { ok: true };
  167. }
  168. case 'STEP_COMPLETE': {
  169. await setStepStatus(message.step, 'completed');
  170. await addLog(`Step ${message.step} completed`, 'ok');
  171. // Store step-specific data
  172. await handleStepData(message.step, message.payload);
  173. return { ok: true };
  174. }
  175. case 'STEP_ERROR': {
  176. await setStepStatus(message.step, 'failed');
  177. await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
  178. return { ok: true };
  179. }
  180. case 'GET_STATE': {
  181. return await getState();
  182. }
  183. case 'RESET': {
  184. await resetState();
  185. await addLog('Flow reset', 'info');
  186. return { ok: true };
  187. }
  188. case 'EXECUTE_STEP': {
  189. const step = message.payload.step;
  190. // Save email if provided (from side panel step 3)
  191. if (message.payload.email) {
  192. await setState({ email: message.payload.email });
  193. }
  194. await executeStep(step);
  195. return { ok: true };
  196. }
  197. // Side panel data updates
  198. case 'SAVE_EMAIL': {
  199. await setState({ email: message.payload.email });
  200. return { ok: true };
  201. }
  202. default:
  203. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  204. return { error: `Unknown message type: ${message.type}` };
  205. }
  206. }
  207. // ============================================================
  208. // Step Data Handlers
  209. // ============================================================
  210. async function handleStepData(step, payload) {
  211. switch (step) {
  212. case 1:
  213. if (payload.oauthUrl) {
  214. await setState({ oauthUrl: payload.oauthUrl });
  215. // Broadcast OAuth URL to side panel
  216. chrome.runtime.sendMessage({
  217. type: 'DATA_UPDATED',
  218. payload: { oauthUrl: payload.oauthUrl },
  219. }).catch(() => {});
  220. }
  221. break;
  222. case 3:
  223. if (payload.email) await setState({ email: payload.email });
  224. break;
  225. case 4:
  226. if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
  227. break;
  228. case 8:
  229. if (payload.localhostUrl) {
  230. await setState({ localhostUrl: payload.localhostUrl });
  231. chrome.runtime.sendMessage({
  232. type: 'DATA_UPDATED',
  233. payload: { localhostUrl: payload.localhostUrl },
  234. }).catch(() => {});
  235. }
  236. break;
  237. }
  238. }
  239. // ============================================================
  240. // Step Execution
  241. // ============================================================
  242. async function executeStep(step) {
  243. console.log(LOG_PREFIX, `Executing step ${step}`);
  244. await setStepStatus(step, 'running');
  245. await addLog(`Step ${step} started`);
  246. const state = await getState();
  247. // Set flow start time on first step
  248. if (step === 1 && !state.flowStartTime) {
  249. await setState({ flowStartTime: Date.now() });
  250. }
  251. try {
  252. switch (step) {
  253. case 1: await executeStep1(state); break;
  254. case 2: await executeStep2(state); break;
  255. case 3: await executeStep3(state); break;
  256. case 4: await executeStep4(state); break;
  257. case 5: await executeStep5(state); break;
  258. case 6: await executeStep6(state); break;
  259. case 7: await executeStep7(state); break;
  260. case 8: await executeStep8(state); break;
  261. case 9: await executeStep9(state); break;
  262. default:
  263. throw new Error(`Unknown step: ${step}`);
  264. }
  265. } catch (err) {
  266. await setStepStatus(step, 'failed');
  267. await addLog(`Step ${step} failed: ${err.message}`, 'error');
  268. }
  269. }
  270. // ============================================================
  271. // Step 1: Get OAuth Link (via vps-panel.js)
  272. // ============================================================
  273. async function executeStep1(state) {
  274. await sendToContentScript('vps-panel', {
  275. type: 'EXECUTE_STEP',
  276. step: 1,
  277. source: 'background',
  278. payload: {},
  279. });
  280. }
  281. // ============================================================
  282. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  283. // ============================================================
  284. async function executeStep2(state) {
  285. if (!state.oauthUrl) {
  286. throw new Error('No OAuth URL. Complete step 1 first.');
  287. }
  288. await addLog(`Step 2: Opening auth URL in new tab: ${state.oauthUrl.slice(0, 80)}...`);
  289. const tab = await chrome.tabs.create({ url: state.oauthUrl, active: true });
  290. // signup-page.js will auto-inject via manifest content_scripts
  291. // Queue the command — it will flush when script sends READY signal
  292. await sendToContentScript('signup-page', {
  293. type: 'EXECUTE_STEP',
  294. step: 2,
  295. source: 'background',
  296. payload: {},
  297. });
  298. }
  299. // ============================================================
  300. // Step 3: Fill Email & Password (via signup-page.js)
  301. // ============================================================
  302. async function executeStep3(state) {
  303. if (!state.email) {
  304. throw new Error('No email address. Paste email in Side Panel first.');
  305. }
  306. await addLog(`Step 3: Filling email ${state.email} and password`);
  307. await sendToContentScript('signup-page', {
  308. type: 'EXECUTE_STEP',
  309. step: 3,
  310. source: 'background',
  311. payload: { email: state.email },
  312. });
  313. }
  314. // ============================================================
  315. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  316. // ============================================================
  317. async function executeStep4(state) {
  318. // Ensure QQ Mail tab is open
  319. const alive = await isTabAlive('qq-mail');
  320. if (!alive) {
  321. await addLog('Step 4: Opening QQ Mail...');
  322. await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true });
  323. } else {
  324. const tabId = await getTabId('qq-mail');
  325. if (tabId) await chrome.tabs.update(tabId, { active: true });
  326. }
  327. // Send poll command to qq-mail
  328. const result = await sendToContentScript('qq-mail', {
  329. type: 'POLL_EMAIL',
  330. step: 4,
  331. source: 'background',
  332. payload: {
  333. filterAfterTimestamp: state.flowStartTime || 0,
  334. senderFilters: ['openai', 'noreply', 'verify', 'auth'],
  335. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
  336. maxAttempts: 20,
  337. intervalMs: 3000,
  338. },
  339. });
  340. if (result && result.error) {
  341. throw new Error(result.error);
  342. }
  343. if (result && result.code) {
  344. await setState({ lastEmailTimestamp: result.emailTimestamp });
  345. await addLog(`Step 4: Got verification code: ${result.code}`);
  346. // Switch to signup tab and fill code
  347. const signupTabId = await getTabId('signup-page');
  348. if (signupTabId) {
  349. await chrome.tabs.update(signupTabId, { active: true });
  350. await sendToContentScript('signup-page', {
  351. type: 'FILL_CODE',
  352. step: 4,
  353. source: 'background',
  354. payload: { code: result.code },
  355. });
  356. } else {
  357. throw new Error('Signup page tab was closed. Cannot fill verification code.');
  358. }
  359. }
  360. }
  361. // ============================================================
  362. // Step 5: Fill Name & Birthday (via signup-page.js)
  363. // ============================================================
  364. async function executeStep5(state) {
  365. const { firstName, lastName } = generateRandomName();
  366. const { year, month, day } = generateRandomBirthday();
  367. await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
  368. await sendToContentScript('signup-page', {
  369. type: 'EXECUTE_STEP',
  370. step: 5,
  371. source: 'background',
  372. payload: { firstName, lastName, year, month, day },
  373. });
  374. }
  375. // ============================================================
  376. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  377. // ============================================================
  378. async function executeStep6(state) {
  379. const alive = await isTabAlive('chatgpt');
  380. if (!alive) {
  381. await addLog('Step 6: Opening ChatGPT...');
  382. await chrome.tabs.create({ url: 'https://chatgpt.com/', active: true });
  383. } else {
  384. const tabId = await getTabId('chatgpt');
  385. if (tabId) await chrome.tabs.update(tabId, { active: true });
  386. }
  387. await sendToContentScript('chatgpt', {
  388. type: 'EXECUTE_STEP',
  389. step: 6,
  390. source: 'background',
  391. payload: {},
  392. });
  393. }
  394. // ============================================================
  395. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  396. // ============================================================
  397. async function executeStep7(state) {
  398. const alive = await isTabAlive('qq-mail');
  399. if (!alive) {
  400. await addLog('Step 7: Opening QQ Mail...');
  401. await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true });
  402. } else {
  403. const tabId = await getTabId('qq-mail');
  404. if (tabId) await chrome.tabs.update(tabId, { active: true });
  405. }
  406. const result = await sendToContentScript('qq-mail', {
  407. type: 'POLL_EMAIL',
  408. step: 7,
  409. source: 'background',
  410. payload: {
  411. filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
  412. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'],
  413. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  414. maxAttempts: 20,
  415. intervalMs: 3000,
  416. },
  417. });
  418. if (result && result.error) {
  419. throw new Error(result.error);
  420. }
  421. if (result && result.code) {
  422. await addLog(`Step 7: Got login verification code: ${result.code}`);
  423. // Switch to ChatGPT tab and fill code
  424. const chatgptTabId = await getTabId('chatgpt');
  425. if (chatgptTabId) {
  426. await chrome.tabs.update(chatgptTabId, { active: true });
  427. await sendToContentScript('chatgpt', {
  428. type: 'FILL_CODE',
  429. step: 7,
  430. source: 'background',
  431. payload: { code: result.code },
  432. });
  433. } else {
  434. throw new Error('ChatGPT tab was closed. Cannot fill verification code.');
  435. }
  436. }
  437. }
  438. // ============================================================
  439. // Step 8: Complete OAuth (webNavigation listener + chatgpt.js navigates)
  440. // ============================================================
  441. let webNavListener = null;
  442. async function executeStep8(state) {
  443. if (!state.oauthUrl) {
  444. throw new Error('No OAuth URL. Complete step 1 first.');
  445. }
  446. await addLog('Step 8: Setting up localhost redirect listener...');
  447. // Register webNavigation listener (scoped to this step)
  448. return new Promise((resolve, reject) => {
  449. const timeout = setTimeout(() => {
  450. if (webNavListener) {
  451. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  452. webNavListener = null;
  453. }
  454. setStepStatus(8, 'failed');
  455. addLog('Step 8: Localhost redirect not captured after 30s. Check if OAuth authorization completed.', 'error');
  456. reject(new Error('Localhost redirect not captured after 30s. Check if OAuth authorization completed.'));
  457. }, 30000);
  458. webNavListener = (details) => {
  459. if (details.url.startsWith('http://localhost')) {
  460. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  461. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  462. webNavListener = null;
  463. clearTimeout(timeout);
  464. setState({ localhostUrl: details.url }).then(() => {
  465. addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
  466. setStepStatus(8, 'completed');
  467. // Broadcast to side panel
  468. chrome.runtime.sendMessage({
  469. type: 'DATA_UPDATED',
  470. payload: { localhostUrl: details.url },
  471. }).catch(() => {});
  472. resolve();
  473. });
  474. }
  475. };
  476. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  477. // Tell chatgpt.js to navigate to OAuth URL
  478. sendToContentScript('chatgpt', {
  479. type: 'EXECUTE_STEP',
  480. step: 8,
  481. source: 'background',
  482. payload: {},
  483. }).catch(err => {
  484. clearTimeout(timeout);
  485. if (webNavListener) {
  486. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  487. webNavListener = null;
  488. }
  489. reject(err);
  490. });
  491. });
  492. }
  493. // ============================================================
  494. // Step 9: VPS Verify (via vps-panel.js)
  495. // ============================================================
  496. async function executeStep9(state) {
  497. if (!state.localhostUrl) {
  498. throw new Error('No localhost URL. Complete step 8 first.');
  499. }
  500. // Switch to VPS panel tab
  501. const alive = await isTabAlive('vps-panel');
  502. if (!alive) {
  503. await addLog('Step 9: Opening VPS panel...');
  504. await chrome.tabs.create({ url: 'http://154.26.182.181:8317/management.html#/oauth', active: true });
  505. } else {
  506. const tabId = await getTabId('vps-panel');
  507. if (tabId) await chrome.tabs.update(tabId, { active: true });
  508. }
  509. await sendToContentScript('vps-panel', {
  510. type: 'EXECUTE_STEP',
  511. step: 9,
  512. source: 'background',
  513. payload: {},
  514. });
  515. }
  516. // ============================================================
  517. // Open Side Panel on extension icon click
  518. // ============================================================
  519. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });