【半自动轮椅♿️】分享一个AA搜JL票脚本,我太太太奶奶都会用

Update:


去年AA大幅增强风控,导致之前大佬分享的全自动脚本不再能用,会一直报错说access blocked。

基于这个稍微改了几个版本,这里分享个半自动,但也是用起来最最方便的轮椅,至少大家能自己动手丰衣足食


原理其实很简单,现在搜票api不能单独发了,需要cookie和credentials,我们直接从chrome里偷来发api reqest。

注意事项

  1. 还是不能发太多request,暂时没试出来准确limit,同ip发多了会被封ip+brower,换个brower能破。被封的过段时间也会自己解开,具体时间不记得了,好像是一天?

  2. 这个需要手动自己每次跑,跑的时候重新execute。也可以改成loop,但是cookie可能会过期,而且被封的概率大幅增加

使用步骤

  1. 打开 aa.com

  2. F12打开console,第一次execute command 需要手动允许一下

  3. 复制这段代码跑就行了,记得改下参数, 在最上面几行,比较self explanatory。 主要就是改你的起飞机场和日期

    1. // === AA Award Calendar Scanner - Run in browser console on aa.com ===
      
      (async function() {
          // ================== CONFIGURATION ==================
          const fromYear = 2026;
          const fromMonth = 2;   // January = 1
          const toMonth = 1;    //
          const origins = ['JFK', 'TYO'];
          const destinations = ['TYO', 'JFK'];
          const cabin = 'BUSINESS,FIRST';  // Options: "COACH", "PREMIUM_COACH", "BUSINESS,FIRST"
          const maxStops = '1';            // "0", "1", "2"
          const includeLink = true;
          const delayMs = 300;             // Delay between requests (ms) - do not set to 0
          // ===================================================
      
          const TYO = new Set(['NRT', 'HND']);
      
          function isSameCity(a, b) {
              return a === b || (TYO.has(a) && TYO.has(b));
          }
      
          // Build list of dates to query (first day of each month)
          const dates = [];
          for (let year = fromYear; year <= (fromMonth <= toMonth ? fromYear : fromYear + 1); year++) {
              const startM = year === fromYear ? fromMonth : 1;
              const endM = year === fromYear && fromMonth <= toMonth ? toMonth : (year > fromYear ? toMonth : 12);
              for (let month = startM; month <= endM; month++) {
                  dates.push(`${year}-${String(month).padStart(2, '0')}-01`);
              }
          }
      
          console.log(`Starting scan: ${dates.length} months × ${origins.length} origins × ${destinations.length} destinations / 2 (roundtrip) = ${dates.length * origins.length * destinations.length/2} total requests`);
      
          const results = [];
          let totalRequests = 0;
      
          for (const depDate of dates) {
              for (const origin of origins) {
                  for (const dest of destinations) {
                      if (isSameCity(origin, dest)) continue;
      
                      totalRequests++;
                      await new Promise(r => setTimeout(r, delayMs));
      
                      let foundInThisMonth = 0;
      
                      try {
                          const response = await fetch("https://www.aa.com/booking/api/search/calendar", {
                              "headers": {
                                  "accept": "application/json, text/plain, */*",
                                  "accept-language": "en-US",
                                  "content-type": "application/json",
                                  "cache-control": "no-cache",
                                  "pragma": "no-cache",
                                  "priority": "u=1, i",
                                  "sec-ch-ua": "\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\"",
                                  "sec-ch-ua-mobile": "?0",
                                  "sec-ch-ua-platform": "\"macOS\"",
                                  "sec-fetch-dest": "empty",
                                  "sec-fetch-mode": "cors",
                                  "sec-fetch-site": "same-origin"
                              },
                              "referrer": "https://www.aa.com/booking/choose-flights",
                              "body": JSON.stringify({
                                  "metadata": { "selectedProducts": [], "tripType": "OneWay", "udo": {} },
                                  "passengers": [{ "type": "adult", "count": 1 }],
                                  "requestHeader": { "clientId": "AAcom" },
                                  "slices": [{
                                      "allCarriers": true,
                                      "cabin": cabin,
                                      "departureDate": depDate,
                                      "destination": dest,
                                      "destinationNearbyAirports": false,
                                      "maxStops": maxStops,
                                      "origin": origin,
                                      "originNearbyAirports": false
                                  }],
                                  "tripOptions": {
                                      "corporateBooking": false,
                                      "fareType": "Lowest",
                                      "locale": "en_US",
                                      "pointOfSale": null,
                                      "searchType": "Award"
                                  },
                                  "loyaltyInfo": null,
                                  "version": "",
                                  "queryParams": { "sliceIndex": 0, "sessionId": "", "solutionSet": "", "solutionId": "" }
                              }),
                              "method": "POST",
                              "mode": "cors",
                              "credentials": "include"
                          });
      
                          if (!response.ok) {
                              console.log(`⚠️ ${depDate} ${origin}→${dest}: HTTP ${response.status} (skipped)`);
                              continue;
                          }
      
                          const data = await response.json();
      
                          for (const month of data.calendarMonths || []) {
                              for (const week of month.weeks || []) {
                                  for (const day of week.days || []) {
                                      if (!day.validDay) continue;
                                      const solution = day.solution;
                                      if (solution && solution.perPassengerAwardPoints < 100000) {
                                          foundInThisMonth++;
                                          const bookUrl = `https://www.aa.com/booking/search?type=OneWay&searchType=Award&from=${origin}&to=${dest}&pax=1&cabin=${cabin}&locale=en_US&nearbyAirports=false&depart=${day.date}&carriers=ALL&pos=US&adult=1`;
                                          
                                          // Detailed find with full link
                                          console.log(`🎯 Found: ${day.date} (${new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' })}) ${origin}→${dest} | ${solution.perPassengerAwardPoints.toLocaleString()} points + $${solution.perPassengerSaleTotal?.amount || 0} | ${bookUrl}`);
                                          
                                          results.push({
                                              date: day.date,
                                              route: `${origin} → ${dest}`,
                                              dayOfWeek: new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' }),
                                              points: solution.perPassengerAwardPoints.toLocaleString(),
                                              cash: solution.perPassengerSaleTotal?.amount || 0,
                                              currency: solution.perPassengerSaleTotal?.currency || 'USD',
                                              link: includeLink ? `<a href="${bookUrl}" target="_blank">Book</a>` : ''
                                          });
                                      }
                                  }
                              }
                          }
      
                      } catch (err) {
                          console.log(`💥 Error on ${depDate} ${origin}→${dest}: ${err.message}`);
                      }
      
                      // === PROGRESS UPDATE FOR THIS MONTH/ROUTE ===
                      if (foundInThisMonth > 0) {
                          console.log(`✅ ${depDate} ${origin}→${dest}: Found ${foundInThisMonth} award dates`);
                      } else {
                          console.log(`❌ ${depDate} ${origin}→${dest}: No availability`);
                      }
                  }
              }
          }
      
          // ============= DISPLAY FINAL RESULTS =============
          console.log(`\nScan complete! Processed ${totalRequests} requests.`);
      
          if (results.length === 0) {
              document.body.insertAdjacentHTML('beforeend', '<h2 style="color:orange">No award availability found in the scanned period 😔</h2>');
              return;
          }
      
          results.sort((a, b) => a.date.localeCompare(b.date));
      
          let tableHTML = `
          <h2 style="color:green">🎉 Found ${results.length} Award Opportunities!</h2>
          <p><strong>Scanned period:</strong> ${dates[0]} to ${dates[dates.length-1].slice(0,7)}-31</p>
          <table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse; font-family:Arial; background:white; margin-top:20px;">
              <thead style="background:#f0f0f0">
                  <tr>
                      <th>Date</th>
                      <th>Day</th>
                      <th>Route</th>
                      <th>Points</th>
                      <th>+ Cash</th>
                      <th>Link</th>
                  </tr>
              </thead>
              <tbody>`;
      
          for (const r of results) {
              tableHTML += `
                  <tr>
                      <td>${r.date}</td>
                      <td>${r.dayOfWeek}</td>
                      <td>${r.route}</td>
                      <td style="text-align:right; font-weight:bold">${r.points}</td>
                      <td style="text-align:right">${r.cash} ${r.currency}</td>
                      <td>${r.link}</td>
                  </tr>`;
          }
      
          tableHTML += `</tbody></table>`;
      
          document.body.insertAdjacentHTML('afterbegin', tableHTML);
      
          console.log(`${results.length} total awards displayed in table above.`);
      })();
      
      

效果演示

Screenshot 2026-01-02 at 5.55.55 AM

Screenshot 2026-01-02 at 5.55.55 AM1920×1013 403 KB

希望大家新年都能找到属于自己的票!

128 个赞

捉个虫 感觉应该是browser(?)

大半夜写的有点神智不清 感谢错字侠出警

牛的兄弟

感谢!找AI改成了bookmarklet,放在书签栏点击运行。

2 个赞

强! 我今天点赞到上限了…

不介意的话可以分享下,我link进主楼

估计还有很多能优化的地方 晚点我再改改!

1 个赞

昨晚我太太太奶奶托梦让我烧一台好点的电脑下去。

竟然还有上限!从来没到过,上限是多少?

y/m/d里的day是不是还要改一下 现在貌似hard code成只搜每个月1号?

hmm 我发的这个版本脚本loop可能没问题,但也不推荐,因为我自己也被block过

我还有一版是会去qurey individual flight 去看机型,那个很容易死。两圈下来可能也就200个request 5min,大概跑十几分钟就无了。不知道是不是我哪里写的有问题。

他们用的好像是akami有点随机 我一直找不到规律

我其实问的是点赞上限

1 个赞

哦哦 你说点赞吗(

我是50 可能我等级太菜了

我设置5秒一次,origin和destination 没动总共24个query,到第11个月的时候就被ban了(400)

只能说NB

loop的话是很有可能ban,但也玄学。这十一个月就ban可能倒霉吧…

不对,看了眼我截图但我好像12月也有400,但我怎么感觉可能是哪里有bug…你再跑一圈试试,有没有成功的? ban应该是access denied 403,而且网页上也会白屏写access denied

请允许我问一个小白问题,打开F12去哪里跑这个command?我找到了run - command, 没用啊

打开后右手边最上面or中间应该有个bar可以选tab,选console,滚到最下面有个地方能打字,复制到那里面后按enter。

如果chome console是第一次跑代码跑会报错,会说要你手动输入一行啥,我不记得了。输入后再复制进去跑一次就行

我因为用的是incognito,之前关了,又开了个新的是能跑的

没什么好说的,牛就一个字!

这什么银卡小号冲钛帖?:troll: