Skip to content

[Bug]: installInterceptor not working after goto a new page #584

@iamrockrepublic

Description

@iamrockrepublic

Description

尝试为ctrip构建机票查询能力

使用页面:https://flights.ctrip.com/online/list/oneway-bjs-sha?_=1&depdate=2026-03-30&cabin=Y_S_C_F
希望使用拦截器模式来拦截batchSearch接口来获取所有航班信息,但goto到该页面后立刻installInterceptor无法正常拦截到接口内容。

Steps to Reproduce

  1. put this file as flight-tickets.ts at src/clis/ctrip/flight-tickets.ts
  2. build: npm run build
  3. run: ./dist/main.js ctrip flight-tickets --from bjs --to sha --date 2026-03-30 --limit 5 -v
  4. returned no data
    [Verbose] Warning: Command returned an empty result.
    (no data)
import { cli, Strategy } from '../../registry.js';

cli({
  site: 'ctrip',
  name: 'flight-tickets',
  description: '查询携程国内机票(单程/往返,含价格、舱位信息)',
  domain: 'flights.ctrip.com',
  strategy: Strategy.INTERCEPT,
  browser: true,
  args: [
    { name: 'from', required: true, help: '出发城市代码(如 bjs=北京,sha=上海,ctu=成都,szx=深圳,can=广州)' },
    { name: 'to', required: true, help: '到达城市代码(如 bjs=北京,sha=上海,ctu=成都,szx=深圳,can=广州)' },
    { name: 'date', required: true, help: '出发日期(格式:YYYY-MM-DD)' },
    { name: 'return_date', help: '返程日期(填写则查询往返票,格式:YYYY-MM-DD)' },
    { name: 'cabin', default: 'Y_S_C_F', help: '舱位类型(Y_S_C_F=不限, Y_S=经济/超经, C_F=公务/头等)' },
    { name: 'limit', type: 'int', default: 15, help: '返回结果数量' },
    { name: 'sort', default: 'price', help: '排序方式(price=价格,time=时间)' },
  ],
  columns: ['rank', 'airline', 'flightNo', 'departure', 'arrival', 'duration', 'price', 'cabin'],
  func: async (page, args) => {
    const { from, to, date, return_date, cabin, limit, sort } = args;

    // 1. 构建搜索URL
    const tripType = return_date ? 'round' : 'oneway';
    const url = tripType === 'round'
      ? `https://flights.ctrip.com/online/list/round-${from}-${to}?depdate=${date}_${return_date}&cabin=${cabin}`
      : `https://flights.ctrip.com/online/list/oneway-${from}-${to}?depdate=${date}&cabin=${cabin}`;

    // 2. 导航到搜索页(不等待页面完全加载)
    await page.goto(url, { waitUntil: 'none' });

    // 3. 立即安装拦截器(在页面开始加载时就挂载)
    await page.installInterceptor('batchSearch');

    // 4. 等待页面加载和API响应
    await page.wait(10);

    // 5. 获取拦截的请求
    const requests = await page.getInterceptedRequests();

    if (!requests || requests.length === 0) {
      return [];
    }

    // 6. 解析航班数据
    const results: any[] = [];

    for (const req of requests) {
      const responseData = req.data;
      if (!responseData) continue;

      const flightItineraryList = responseData.flightItineraryList || [];

      for (const item of flightItineraryList) {
        if (results.length >= Number(limit)) break;

        const flightSegments = item.flightSegments || [];
        if (!flightSegments.length) continue;

        // 处理第一段行程(去程)
        const firstSegment = flightSegments[0];
        const flightList = firstSegment.flightList || [];
        if (!flightList.length) continue;

        const flight = flightList[0];
        const priceInfo = item.priceInfo || {};

        // 格式化起飞和到达时间
        const departureDate = flight.departureDate || '';
        const departureTime = flight.departureTime || '';
        const arrivalDate = flight.arrivalDate || '';
        const arrivalTime = flight.arrivalTime || '';

        // 组装起飞信息
        const departureInfo = [
          departureTime,
          flight.departureAirportName || '',
          flight.departureTerminal ? `T${flight.departureTerminal}` : ''
        ].filter(Boolean).join(' ');

        // 组装到达信息
        const arrivalInfo = [
          arrivalTime,
          flight.arrivalAirportName || '',
          flight.arrivalTerminal ? `T${flight.arrivalTerminal}` : ''
        ].filter(Boolean).join(' ');

        // 添加日期标识(如果跨天)
        const arrivalDateSuffix = departureDate !== arrivalDate ? ' +1' : '';

        // 计算飞行时长
        const duration = firstSegment.duration ? `${Math.floor(Number(firstSegment.duration) / 60)}h${Number(firstSegment.duration) % 60}m` : '-';

        // 获取舱位信息
        const cabinInfo = priceInfo.cabinClass || '经济舱';

        // 价格信息
        const adultPrice = priceInfo.adultPrice;
        const priceDisplay = adultPrice ? ${adultPrice}` : '-';

        // 航司和航班号
        const airlineName = flight.airlineName || '';
        const flightNo = flight.flightNo || '';

        results.push({
          rank: results.length + 1,
          airline: airlineName,
          flightNo: flightNo,
          departure: departureInfo,
          arrival: arrivalInfo + arrivalDateSuffix,
          duration: duration,
          price: priceDisplay,
          cabin: cabinInfo,
          _sortPrice: adultPrice || 999999, // 用于排序
          _sortTime: departureTime || '23:59' // 用于排序
        });
      }
    }

    // 7. 排序
    if (sort === 'price') {
      results.sort((a, b) => a._sortPrice - b._sortPrice);
    } else if (sort === 'time') {
      results.sort((a, b) => a._sortTime.localeCompare(b._sortTime));
    }

    // 8. 更新排名并移除排序字段
    return results.map((item, index) => {
      const { _sortPrice, _sortTime, ...rest } = item;
      return { ...rest, rank: index + 1 };
    });
  },
});

Expected Behavior

直接使用CDP可以拦截到接口内容以及航班字段flightItineraryList,希望使用opencli的插件拦截器也能达到该效果

### Result
{"found":2,"urls":["https://flights.ctrip.com/international/search/api/search/batchSearch?v=0.48258055969123614","https://flights.ctrip.com/international/search/api/flight/comfort/batchGetComfortTagList?v=0.055198922744281065"],"sampleData":{"keys":["status","msg","data"],"dataPath":["context","flightItineraryList","giftIds","bestChoiceFlightsForceTop","recommendProduct"]}}
### Ran Playwright code
```js
await (async (page) => {
  const batchSearchRequests = [];
  
  const client = await page.context().newCDPSession(page);
  await client.send('Network.enable');
  
  client.on('Network.responseReceived', (params) => {
    const url = params.response.url;
    // 查找包含 batchSearch 的请求
    if (url.toLowerCase().includes('batchsearch') || url.toLowerCase().includes('batch')) {
      batchSearchRequests.push({
        url: url,
        status: params.response.status,
        requestId: params.requestId
      });
    }
  });
  
  await page.goto('https://flights.ctrip.com/online/list/oneway-bjs-sha?depdate=2026-03-30&cabin=Y_S_C_F');
  await page.waitForTimeout(10000);
  
  // 尝试获取第一个 batchSearch 响应的内容
  let responseBody = null;
  if (batchSearchRequests.length > 0) {
    try {
      const response = await client.send('Network.getResponseBody', {
        requestId: batchSearchRequests[0].requestId
      });
      responseBody = JSON.parse(response.body);
    } catch (e) {
      responseBody = { error: e.message };
    }
  }
  
  return {
    found: batchSearchRequests.length,
    urls: batchSearchRequests.map(r => r.url),
    sampleData: responseBody ? {
      keys: Object.keys(responseBody),
      dataPath: responseBody.data ? Object.keys(responseBody.data).slice(0, 5) : null
    } : null
  };
})(page);

Page

Events

  • New console entries: .playwright-mcp/console-2026-03-29T13-44-46-347Z.log#L16-L17
  • [LOG] [echo-sdk] init success,traceType= echo glob...atic/chunks/ctrip-package-async.43fa578eb78a151e.js:0
  • [LOG] [echo-sdk] this is test api! @ https://bd-s....atic/chunks/ctrip-package-async.43fa578eb78a151e.js:0
  • [LOG] [echo-sdk] echoTraceOfNormal@1.1.11 A0601@PR...atic/chunks/ctrip-package-async.43fa578eb78a151e.js:0
  • [WARNING] The PerformanceObserver does not support...n.com/packages/flight/coffeebean-web/5.2.8/main.js:28
  • [LOG] [WEB-Core] Init. @ https://bd-s.tripcdn.cn/m.../_next/static/chunks/pages/_app-dfe6772f154fd592.js:0
  • [INFO] [Ares] version 2.8.92 @ https://bd-s.tripcd.../_next/static/chunks/pages/_app-dfe6772f154fd592.js:0
  • [INFO] [Ares] assetPrefix https://bd-s.tripcdn.cn .../_next/static/chunks/pages/_app-dfe6772f154fd592.js:0
  • [INFO] [Ares] modules {group: flight, name: flight.../_next/static/chunks/pages/_app-dfe6772f154fd592.js:0
  • [INFO] [Ares] manifest {/online/_next/static/chunk.../_next/static/chunks/pages/_app-dfe6772f154fd592.js:0
  • [LOG] %c[logDevTrace] color:#cc2b2b {type: sdkLoad...oginsdkv2/%5E1.5.3/default/pcloginsdk.js?expires=1d:1
  • [LOG] %c[logMonitor] color:#e95c0b success {group:...oginsdkv2/%5E1.5.3/default/pcloginsdk.js?expires=1d:1
  • [WARNING] An iframe which has both allow-scripts a...attribute can escape its sandboxing. @ about:srcdoc:0
  • [LOG] %c[logDevTrace] color:#cc2b2b {type: detectI...oginsdkv2/%5E1.5.3/default/pcloginsdk.js?expires=1d:1
  • [LOG] {ResponseStatus: Object, tipsInfo: null, res...om/NFES/mfe_compose/1773049485574/compose.preact.js:2
  • [LOG] [echo-sdk] init success,traceType= echo glob...atic/chunks/ctrip-package-async.43fa578eb78a151e.js:0
  • [LOG] [echo-sdk] this is test api! @ https://bd-s....atic/chunks/ctrip-package-async.43fa578eb78a151e.js:0
  • [LOG] [echo-sdk] echoTraceOfNormal@1.1.11 A0601@PR...atic/chunks/ctrip-package-async.43fa578eb78a151e.js:0

### OpenCLI Version

1.5.2

### Node.js Version

22.x

### Operating System

macOS

### Logs / Screenshots

```shell

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions