checkout-api-utils.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // background/checkout-api-utils.js — API helpers for Stripe/PayPal checkout automation
  2. (function attachCheckoutApiUtils(root) {
  3. root.MultiPageCheckoutApiUtils = (function createCheckoutApiUtilsModule() {
  4. const ADDRESS_API_URL = 'https://www.meiguodizhi.com/api/v1/dz';
  5. const CHATGPT_CHECKOUT_URL = 'https://chatgpt.com/backend-api/payments/checkout';
  6. const DEFAULT_PAYPAL_SMS_MAX_ATTEMPTS = 18;
  7. const DEFAULT_PAYPAL_SMS_INTERVAL_MS = 5000;
  8. /**
  9. * Fetch a random US address from meiguodizhi.com.
  10. * @returns {Promise<{street: string, city: string, state: string, zip: string}>}
  11. */
  12. async function fetchRandomAddress() {
  13. try {
  14. const response = await fetch(ADDRESS_API_URL, {
  15. method: 'POST',
  16. headers: { 'Content-Type': 'application/json' },
  17. body: JSON.stringify({ path: '/', method: 'address' }),
  18. });
  19. const d = await response.json();
  20. const a = d.address || d;
  21. return {
  22. street: a.Address || a.street || '123 Main St',
  23. city: a.City || a.city || 'New York',
  24. state: a.State_Full || a.State || a.state || 'New York',
  25. zip: String(a.Zip_Code || a.zip || '10001').substring(0, 5),
  26. };
  27. } catch (e) {
  28. console.error('[CheckoutAPI] 获取随机地址失败:', e.message);
  29. return { street: '123 Main St', city: 'New York', state: 'New York', zip: '10001' };
  30. }
  31. }
  32. /**
  33. * Generate a ChatGPT Plus Stripe hosted checkout URL.
  34. * @param {string} accessToken - ChatGPT session access token
  35. * @returns {Promise<{hostedUrl: string, checkoutSessionId: string}|null>}
  36. */
  37. async function generateCheckoutLink(accessToken) {
  38. try {
  39. const payload = {
  40. plan_name: 'chatgptplusplan',
  41. billing_details: { country: 'US', currency: 'USD' },
  42. cancel_url: 'https://chatgpt.com/#pricing',
  43. promo_campaign: { promo_campaign_id: 'plus-1-month-free', is_coupon_from_query_param: false },
  44. checkout_ui_mode: 'hosted',
  45. };
  46. const response = await fetch(CHATGPT_CHECKOUT_URL, {
  47. method: 'POST',
  48. headers: {
  49. 'Authorization': `Bearer ${accessToken}`,
  50. 'Content-Type': 'application/json',
  51. },
  52. body: JSON.stringify(payload),
  53. });
  54. const data = await response.json();
  55. if (!response.ok) {
  56. console.error('[CheckoutAPI] 请求失败 HTTP', response.status, data);
  57. return null;
  58. }
  59. const hostedUrl = data?.url || data?.stripe_hosted_url || data?.checkout_url;
  60. if (!hostedUrl) {
  61. console.error('[CheckoutAPI] 未找到长链接', data);
  62. return null;
  63. }
  64. return {
  65. hostedUrl,
  66. checkoutSessionId: data.checkout_session_id || '',
  67. };
  68. } catch (e) {
  69. console.error('[CheckoutAPI] 生成 Plus 链接异常:', e.message);
  70. return null;
  71. }
  72. }
  73. function collectSmsTextValues(value, results = []) {
  74. if (value === null || value === undefined) {
  75. return results;
  76. }
  77. if (typeof value === 'string' || typeof value === 'number') {
  78. results.push(String(value));
  79. return results;
  80. }
  81. if (Array.isArray(value)) {
  82. value.forEach((item) => collectSmsTextValues(item, results));
  83. return results;
  84. }
  85. if (typeof value === 'object') {
  86. const preferredKeys = [
  87. 'code',
  88. 'sms',
  89. 'message',
  90. 'msg',
  91. 'content',
  92. 'text',
  93. 'data',
  94. 'result',
  95. ];
  96. preferredKeys.forEach((key) => {
  97. if (Object.prototype.hasOwnProperty.call(value, key)) {
  98. collectSmsTextValues(value[key], results);
  99. }
  100. });
  101. Object.keys(value)
  102. .filter((key) => !preferredKeys.includes(key))
  103. .forEach((key) => collectSmsTextValues(value[key], results));
  104. }
  105. return results;
  106. }
  107. function extractPaypalSmsCode(rawPayload, options = {}) {
  108. const excluded = new Set((options.excludeCodes || []).map((code) => String(code || '').trim()).filter(Boolean));
  109. const rawText = String(rawPayload || '').trim();
  110. if (!rawText) return '';
  111. let values = [rawText];
  112. try {
  113. values = collectSmsTextValues(JSON.parse(rawText));
  114. } catch {
  115. // Plain text API responses are expected.
  116. }
  117. const candidates = [];
  118. values.forEach((value) => {
  119. String(value || '').replace(/\b(\d{4,8})\b/g, (_, code) => {
  120. if (!excluded.has(code)) {
  121. candidates.push(code);
  122. }
  123. return _;
  124. });
  125. });
  126. return candidates.find((code) => code.length === 6)
  127. || candidates.find((code) => code.length >= 4 && code.length <= 8)
  128. || '';
  129. }
  130. async function fetchPaypalSmsCode(options = {}, deps = {}) {
  131. const fetchImpl = deps.fetchImpl || fetch;
  132. const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
  133. const addLog = deps.addLog || (async () => {});
  134. const throwIfStopped = deps.throwIfStopped || (() => {});
  135. const smsApiUrl = String(options.smsApiUrl || '').trim();
  136. if (!smsApiUrl) {
  137. throw new Error('PayPal 短信收码 API 未配置。');
  138. }
  139. const maxAttempts = Math.max(1, Math.floor(Number(options.maxAttempts) || DEFAULT_PAYPAL_SMS_MAX_ATTEMPTS));
  140. const intervalMs = Math.max(500, Math.floor(Number(options.intervalMs) || DEFAULT_PAYPAL_SMS_INTERVAL_MS));
  141. let lastError = '';
  142. for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
  143. throwIfStopped();
  144. try {
  145. const response = await fetchImpl(smsApiUrl, {
  146. method: 'GET',
  147. cache: 'no-store',
  148. });
  149. const text = await response.text();
  150. if (!response.ok) {
  151. lastError = `HTTP ${response.status}: ${text.slice(0, 120)}`;
  152. } else {
  153. const code = extractPaypalSmsCode(text, {
  154. excludeCodes: options.excludeCodes || [],
  155. });
  156. if (code) {
  157. return {
  158. code,
  159. attempt,
  160. raw: text,
  161. };
  162. }
  163. lastError = text.slice(0, 160) || '接口未返回验证码';
  164. }
  165. } catch (error) {
  166. lastError = error?.message || String(error || '请求失败');
  167. }
  168. if (attempt < maxAttempts) {
  169. await addLog(`PayPal 短信验证码暂未获取到,${Math.round(intervalMs / 1000)} 秒后重试(${attempt}/${maxAttempts})。`, 'info');
  170. await sleep(intervalMs);
  171. }
  172. }
  173. throw new Error(`PayPal 短信验证码获取超时。最后响应:${lastError || '无响应'}`);
  174. }
  175. return {
  176. extractPaypalSmsCode,
  177. fetchRandomAddress,
  178. fetchPaypalSmsCode,
  179. generateCheckoutLink,
  180. };
  181. })();
  182. })(typeof self !== 'undefined' ? self : globalThis);