sync-cpa-session.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. // background/steps/sync-cpa-session.js — Step 10: Sync current ChatGPT session to CPA
  2. (function attachBackgroundCpaSessionSync(root, factory) {
  3. root.MultiPageBackgroundCpaSessionSync = factory();
  4. })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundCpaSessionSyncModule() {
  5. const CHATGPT_SESSION_URL = 'https://chatgpt.com/';
  6. const CHATGPT_SESSION_ENDPOINT = 'https://chatgpt.com/api/auth/session';
  7. function createCpaSessionSyncExecutor(deps = {}) {
  8. const {
  9. addLog = async () => {},
  10. chrome = null,
  11. completeStepFromBackground = async () => {},
  12. createCpaApi = null,
  13. fetchImpl = (...args) => fetch(...args),
  14. getPanelMode = (state) => (state?.panelMode === 'sub2api' ? 'sub2api' : 'cpa'),
  15. getTabId = async () => null,
  16. sleepWithStop = sleep,
  17. throwIfStopped = () => {},
  18. waitForTabComplete = async () => null,
  19. } = deps;
  20. let cpaApi = null;
  21. function getApi() {
  22. if (cpaApi) return cpaApi;
  23. const factory = createCpaApi || self.MultiPageBackgroundCpaApi?.createCpaApi;
  24. if (typeof factory !== 'function') {
  25. throw new Error('CPA 接口模块未加载,无法同步当前 ChatGPT 会话。');
  26. }
  27. cpaApi = factory({ addLog, fetchImpl });
  28. return cpaApi;
  29. }
  30. async function executeStep10(state = {}) {
  31. throwIfStopped();
  32. await addLog('步骤 10:正在准备同步 ChatGPT session 到 CPA...');
  33. const mode = getPanelMode(state);
  34. if (mode === 'sub2api') {
  35. await addLog('步骤 10:当前为 SUB2API 模式,跳过 CPA session 同步。', 'warn');
  36. await completeStepFromBackground(10, {
  37. cpaSyncSkipped: true,
  38. cpaSyncSkipReason: 'sub2api-mode',
  39. });
  40. return;
  41. }
  42. if (!normalizeString(state.vpsUrl)) {
  43. throw new Error('步骤 10:尚未配置 CPA 地址,请先在侧边栏填写。');
  44. }
  45. if (!normalizeString(state.vpsPassword)) {
  46. throw new Error('步骤 10:尚未配置 CPA 管理密钥,请先在侧边栏填写。');
  47. }
  48. await addLog('步骤 10:正在读取 ChatGPT 当前登录 session...');
  49. const sessionState = await readCurrentChatGptSession();
  50. throwIfStopped();
  51. await addLog('步骤 10:已读取 ChatGPT session,正在生成 CPA auth JSON 并同步...');
  52. const result = await getApi().importCurrentChatGptSession({
  53. ...state,
  54. session: sessionState.session,
  55. accessToken: sessionState.accessToken,
  56. }, {
  57. logLabel: '步骤 10',
  58. timeoutMs: 120000,
  59. importTimeoutMs: 120000,
  60. });
  61. await completeStepFromBackground(10, result);
  62. }
  63. async function readCurrentChatGptSession() {
  64. try {
  65. return await fetchChatGptSessionFromBackground();
  66. } catch (error) {
  67. await addLog(`步骤 10:后台读取 ChatGPT session 失败,准备打开页面上下文重试:${error.message}`, 'warn');
  68. }
  69. return fetchChatGptSessionFromPage();
  70. }
  71. async function fetchChatGptSessionFromBackground() {
  72. const response = await fetchImpl(CHATGPT_SESSION_ENDPOINT, {
  73. credentials: 'include',
  74. headers: {
  75. Accept: 'application/json',
  76. },
  77. });
  78. const text = await response.text();
  79. let session = null;
  80. try {
  81. session = text ? JSON.parse(text) : null;
  82. } catch {
  83. throw new Error(`Session 响应不是有效 JSON:${text.slice(0, 300)}`);
  84. }
  85. if (!response.ok) {
  86. throw new Error(`Session 请求失败,HTTP ${response.status}: ${JSON.stringify(session)}`);
  87. }
  88. return normalizeSessionResult({ session, accessToken: session?.accessToken }, '后台');
  89. }
  90. async function fetchChatGptSessionFromPage() {
  91. if (!chrome?.tabs || !chrome?.scripting?.executeScript) {
  92. throw new Error('当前环境无法在 ChatGPT 页面上下文读取 session。');
  93. }
  94. const tabId = await resolveChatGptSessionTabId();
  95. if (!tabId) {
  96. throw new Error('未找到可读取 ChatGPT session 的标签页。');
  97. }
  98. await waitForTabComplete(tabId, { timeoutMs: 30000, retryDelayMs: 300 });
  99. await sleepWithStop(1000);
  100. const results = await chrome.scripting.executeScript({
  101. target: { tabId },
  102. func: fetchSessionInChatGptPage,
  103. });
  104. const result = results?.[0]?.result;
  105. if (!result || result.error) {
  106. throw new Error(result?.error || '页面上下文读取 ChatGPT session 失败。');
  107. }
  108. return normalizeSessionResult(result, '页面');
  109. }
  110. async function resolveChatGptSessionTabId() {
  111. const registeredSignupTabId = await getTabId('signup-page').catch(() => null);
  112. const registeredSignupTab = await getChatGptTabIfReadable(registeredSignupTabId);
  113. if (registeredSignupTab?.id) return registeredSignupTab.id;
  114. const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
  115. const activeMatch = pickPreferredChatGptTab(activeTabs);
  116. if (activeMatch?.id) return activeMatch.id;
  117. const chatGptTabs = await chrome.tabs.query({ url: ['https://chatgpt.com/*'] }).catch(() => []);
  118. const existingMatch = pickPreferredChatGptTab(chatGptTabs);
  119. if (existingMatch?.id) return existingMatch.id;
  120. await addLog('步骤 10:未找到 ChatGPT 页面,正在打开 chatgpt.com 以读取 session...', 'info');
  121. const created = await chrome.tabs.create({ url: CHATGPT_SESSION_URL, active: true });
  122. return created?.id || null;
  123. }
  124. async function getChatGptTabIfReadable(tabId) {
  125. const numericTabId = Number(tabId) || 0;
  126. if (!numericTabId || !chrome?.tabs?.get) return null;
  127. const tab = await chrome.tabs.get(numericTabId).catch(() => null);
  128. return tab?.id && isChatGptSessionUrl(tab.url) ? tab : null;
  129. }
  130. function pickPreferredChatGptTab(tabs = []) {
  131. return (Array.isArray(tabs) ? tabs : [])
  132. .filter((tab) => Number.isInteger(tab?.id) && isChatGptSessionUrl(tab.url))
  133. .sort((left, right) => {
  134. const activeDiff = Number(Boolean(right.active)) - Number(Boolean(left.active));
  135. if (activeDiff) return activeDiff;
  136. return (Number(right.lastAccessed) || 0) - (Number(left.lastAccessed) || 0);
  137. })[0] || null;
  138. }
  139. function isChatGptSessionUrl(url = '') {
  140. try {
  141. const parsed = new URL(String(url || ''));
  142. return /^https?:$/i.test(parsed.protocol)
  143. && parsed.hostname.toLowerCase() === 'chatgpt.com';
  144. } catch {
  145. return false;
  146. }
  147. }
  148. function normalizeSessionResult(result = {}, label = 'ChatGPT') {
  149. const session = result?.session && typeof result.session === 'object' && !Array.isArray(result.session)
  150. ? result.session
  151. : null;
  152. const accessToken = normalizeString(result?.accessToken || session?.accessToken);
  153. if (!session && !accessToken) {
  154. throw new Error(`${label} 未返回有效的 ChatGPT session 或 accessToken。`);
  155. }
  156. if (!accessToken) {
  157. throw new Error(`${label} Session 响应中没有 accessToken。`);
  158. }
  159. return { session, accessToken };
  160. }
  161. return {
  162. executeStep10,
  163. fetchChatGptSessionFromBackground,
  164. isChatGptSessionUrl,
  165. };
  166. }
  167. async function fetchSessionInChatGptPage() {
  168. try {
  169. const response = await fetch('/api/auth/session', {
  170. credentials: 'include',
  171. headers: {
  172. Accept: 'application/json',
  173. },
  174. });
  175. const text = await response.text();
  176. let session = null;
  177. try {
  178. session = text ? JSON.parse(text) : null;
  179. } catch {
  180. return { error: `Session 响应不是有效 JSON:${text.slice(0, 300)}` };
  181. }
  182. if (!response.ok) {
  183. return { error: `Session 请求失败,HTTP ${response.status}: ${JSON.stringify(session)}` };
  184. }
  185. return {
  186. session,
  187. accessToken: session?.accessToken || '',
  188. };
  189. } catch (error) {
  190. return { error: error?.message || String(error || '页面上下文读取 ChatGPT session 失败。') };
  191. }
  192. }
  193. function normalizeString(value = '') {
  194. return String(value || '').trim();
  195. }
  196. function sleep(ms) {
  197. return new Promise((resolve) => setTimeout(resolve, ms));
  198. }
  199. return {
  200. createCpaSessionSyncExecutor,
  201. };
  202. });