灵猴市集Linghou Market

携程目的地联想搜索

携程ctrip-destination-search

用途 在携程页面读取目的地联想结果,适合根据关键词获取城市、行政区、车站、地标等轻量候选项。 执行前提 需要先打开 https://www.ctrip.com/ 或其他 ctrip.com 页面后执行。 脚本在浏览器页面主世界请求公开目的地联想接口。若当前页面触发登录、验证码或网络策略限制,可能返回错误;这种情况下先在当前标签页正常完成页面访问后再执行。 …

目的地搜索旅行
版本1.0.0
扫描passed
更新2026-06-28
超时30000ms

匹配范围

*://*.ctrip.com/*

排除范围

未声明

能力声明

dom.read

代码

源码已展开
1return await (async () => {2  const input = readParams();3  const query = toText(input.query);4  if (!query) {5    throw new Error("query is required.");6  }7 8  const limit = readInteger(input.limit, 10, 1, 30, "limit");9  const payload = {10    keyword: query,11    searchType: "D",12    pageSize: Math.max(limit, 15),13    head: {14      cid: "",15      ctok: "",16      cver: "1.0",17      lang: "01",18      sid: "8888",19      syscode: "09",20      auth: "",21      extension: [],22    },23  };24 25  const response = await fetch("https://m.ctrip.com/restapi/soa2/21881/json/gaHotelSearchEngine", {26    method: "POST",27    credentials: "include",28    headers: {29      accept: "application/json, text/plain, */*",30      "content-type": "application/json",31    },32    body: JSON.stringify(payload),33  });34  const text = await response.text();35  if (!response.ok) {36    throw new Error("Request failed with HTTP " + response.status + ".");37  }38 39  let data;40  try {41    data = JSON.parse(text);42  } catch (error) {43    throw new Error("The site returned a non-JSON response. Open the home page and try again.");44  }45 46  if (data && data.Result === false) {47    const message = toText(data.ErrorMsg || data.Message) || "The destination API returned an error.";48    throw new Error(message);49  }50 51  const responseBody = data.Response || data.response || {};52  const items = firstArray([53    responseBody.searchResults,54    responseBody.results,55    responseBody.list,56    data.searchResults,57    data.results,58  ]);59  const results = items.slice(0, limit).map(normalizeDestination);60 61  return {62    query,63    limit,64    count: results.length,65    results,66  };67 68  function readParams() {69    const value = typeof params === "undefined" ? {} : params;70    if (value == null) {71      return {};72    }73    if (typeof value !== "object" || Array.isArray(value)) {74      throw new Error("params must be an object.");75    }76    return value;77  }78 79  function normalizeDestination(item) {80    const latitude = firstNumber(item, ["gdLat", "lat", "gLat", "bdLat"]);81    const longitude = firstNumber(item, ["gdLon", "lon", "gLon", "bdLon"]);82    return {83      id: firstText(item, ["id", "poiId", "locationId"]),84      type: firstText(item, ["type", "resultsType"]),85      name: firstText(item, ["word", "name", "cityName", "displayName"]),86      displayName: firstText(item, ["displayName", "word", "name"]),87      displayType: firstText(item, ["displayType", "type"]),88      cityId: firstNumber(item, ["cityId"]),89      cityName: firstText(item, ["cityName"]),90      provinceName: firstText(item, ["provinceName"]),91      countryId: firstNumber(item, ["countryId"]),92      countryName: firstText(item, ["countryName"]),93      englishName: firstText(item, ["eName", "cityEName", "countryEName"]),94      domestic: item && typeof item.domestic === "boolean" ? item.domestic : null,95      mainland: item && typeof item.mainland === "boolean" ? item.mainland : null,96      latitude,97      longitude,98    };99  }100 101  function firstArray(values) {102    for (const value of values) {103      if (Array.isArray(value)) {104        return value;105      }106    }107    return [];108  }109 110  function firstText(item, keys) {111    for (const key of keys) {112      if (item && item[key] != null && item[key] !== "") {113        return toText(item[key]);114      }115    }116    return "";117  }118 119  function firstNumber(item, keys) {120    for (const key of keys) {121      if (item && item[key] != null && item[key] !== "") {122        const number = Number(item[key]);123        if (Number.isFinite(number)) {124          return number;125        }126      }127    }128    return null;129  }130 131  function readInteger(value, defaultValue, min, max, name) {132    const number = value == null || value === "" ? defaultValue : Number(value);133    if (!Number.isInteger(number) || number < min || number > max) {134      throw new Error(name + " must be an integer between " + min + " and " + max + ".");135    }136    return number;137  }138 139  function toText(value) {140    if (value == null) {141      return "";142    }143    if (Array.isArray(value)) {144      return value.map(toText).filter(Boolean).join(" ");145    }146    return String(value).replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();147  }148})();