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
- put this file as flight-tickets.ts at src/clis/ctrip/flight-tickets.ts
- build: npm run build
- run: ./dist/main.js ctrip flight-tickets --from bjs --to sha --date 2026-03-30 --limit 5 -v
- 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
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
[Verbose] Warning: Command returned an empty result.
(no data)
Expected Behavior
直接使用CDP可以拦截到接口内容以及航班字段flightItineraryList,希望使用opencli的插件拦截器也能达到该效果
Page
Events