Skill: tengjiaozhai/china-rental-price (version 1.0.0)

--- name: china-rental-price description: Use when the user needs a China domestic rental-car reference price, especially for same-day or holiday travel where train-station or airport store choice matters and OneHai peak-period minimum-rental rules can be mistaken for zero inventory. This skill uses the user's logged-in Google Chrome session on macOS to query OneHai, return reference prices and booking URLs, and distinguish policy restrictions from true no-car results. --- # China Rental Price ## Overview This skill is for China domestic self-drive rental price checks in travel-planning conversations. The currently reliable path is OneHai via the user's logged-in Google Chrome session on macOS. ## When To Use - The user asks for a China domestic rental-car price, same-day quote, or holiday quote. - The user wants a station or airport pickup store, not an arbitrary citywide sweep. - The user shows a "0 cars" page and needs to know whether it is truly sold out or blocked by a peak-period minimum-rental rule. ## Preconditions - Use absolute dates and times. - Google Chrome must already be logged into 一嗨租车. - Chrome must have `查看 > 开发者 > 允许 Apple 事件中的 JavaScript` enabled. - macOS needs `osascript` and `tesseract`. ## Workflow 1. Prefer scene-based store targeting: - High-speed rail or train arrival: `train-station` - Flight arrival: `airport` - Only use citywide selection when the user did not specify a transit scene. 2. If the user knows the exact store, pass `--pickup-location` and `--dropoff-location`. 3. Run `scripts/query-onehai-live-chrome.mjs`. 4. Interpret the result before answering: - `bookingRestriction` means a policy limit such as a holiday minimum-rental rule. Do not describe it as ordinary sellout. - `priceMin` and `vehicleSamples` are reference prices captured at query time, not a guaranteed final checkout price. - `availableCars = 0` without `bookingRestriction` is the closer match to true no inventory at that store and time. ## Commands City plus transit scene: ```bash node /Users/shenmingjie/.codex/skills/china-rental-price/scripts/query-onehai-live-chrome.mjs \ --pickup-city 宣城 \ --pickup-scene train-station \ --pickup-datetime 2026-05-01T15:30 \ --dropoff-datetime 2026-05-04T10:30 ``` Exact store: ```bash node /Users/shenmingjie/.codex/skills/china-rental-price/scripts/query-onehai-live-chrome.mjs \ --pickup-city 宣城 \ --pickup-location "泾县高铁站自助点" \ --dropoff-city 宣城 \ --dropoff-location "泾县高铁站自助点" \ --pickup-datetime 2026-05-01T10:00 \ --dropoff-datetime 2026-05-04T10:00 ``` ## Output Shape The script prints JSON with fields such as: - `platform` - `status` - `capturedAt` - `bookingUrl` - `selectedStore` - `priceMin` - `priceTotalIfAvailable` - `availableCars` - `vehicleSamples` - `bookingRestriction` - `warnings` ## Response Guidance - Report the selected store explicitly so the user knows whether the quote came from a train-station store, airport store, or another branch. - Keep the wording as `参考实时价`. - If the query is part of a broader itinerary, combine the rental result with the user's actual arrival mode instead of searching every store in the city. - Be transparent that the reliable automated path in this skill is currently OneHai.

path: scripts/query.mjs
content: import { splitLocalDateTime } from "./utils.mjs";
import { normalizeTransitScene } from "./onehai-policy.mjs";

function normalizeString(value) {
  return value === undefined || value === null ? "" : String(value).trim();
}

function normalizeDateTime(value) {
  const normalized = normalizeString(value).replace("T", " ");
  splitLocalDateTime(normalized);
  return normalized;
}

export function normalizeQuery(input) {
  const pickupCity = normalizeString(input.pickupCity);
  const dropoffCity = normalizeString(input.dropoffCity) || pickupCity;
  const pickupDateTime = normalizeDateTime(input.pickupDateTime);
  const dropoffDateTime = normalizeDateTime(input.dropoffDateTime);

  if (!pickupCity) {
    throw new Error("pickupCity is required");
  }

  if (!pickupDateTime || !dropoffDateTime) {
    throw new Error("pickupDateTime and dropoffDateTime are required");
  }

  return {
    pickup: {
      city: pickupCity,
      location: normalizeString(input.pickupLocation) || null,
      scene: normalizeTransitScene(input.pickupScene, input.pickupLocation)
    },
    dropoff: {
      city: dropoffCity,
      location: normalizeString(input.dropoffLocation) || null,
      scene: normalizeTransitScene(input.dropoffScene, input.dropoffLocation)
    },
    pickupAt: pickupDateTime,
    dropoffAt: dropoffDateTime,
    vehicleClass: normalizeString(input.vehicleClass) || null,
    sameCity: pickupCity === dropoffCity
  };
}

export function queryCacheKey(query) {
  return JSON.stringify(query);
}


path: scripts/utils.mjs
content: import crypto from "node:crypto";

export function sha256(value) {
  return crypto.createHash("sha256").update(value).digest("hex");
}

export function sanitizeFilePart(value) {
  return String(value).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "item";
}

export function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export function nowIso() {
  return new Date().toISOString();
}

export function createTimestampSlug(date = new Date()) {
  return date.toISOString().replace(/[:.]/g, "-");
}

export function splitLocalDateTime(value) {
  const match = String(value).trim().replace("T", " ").match(/^(\d{4}-\d{2}-\d{2}) (\d{2}):(\d{2})$/);
  if (!match) {
    throw new Error(`Invalid local datetime: ${value}`);
  }

  return {
    date: match[1],
    hour: match[2],
    minute: match[3],
    time: `${match[2]}:${match[3]}`
  };
}

export function parseLocalDateTime(value) {
  const parts = splitLocalDateTime(value);
  const [year, month, day] = parts.date.split("-").map(Number);
  const hour = Number(parts.hour);
  const minute = Number(parts.minute);

  return new Date(Date.UTC(year, month - 1, day, hour - 8, minute));
}

export function formatLocalDateTime(date) {
  const localDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
  const year = localDate.getUTCFullYear();
  const month = String(localDate.getUTCMonth() + 1).padStart(2, "0");
  const day = String(localDate.getUTCDate()).padStart(2, "0");
  const hour = String(localDate.getUTCHours()).padStart(2, "0");
  const minute = String(localDate.getUTCMinutes()).padStart(2, "0");

  return `${year}-${month}-${day} ${hour}:${minute}`;
}

export function addDaysToLocalDateTime(value, days) {
  const date = parseLocalDateTime(value);
  return formatLocalDateTime(new Date(date.getTime() + days * 24 * 60 * 60 * 1000));
}

export function getLocalDurationMinutes(startValue, endValue) {
  const start = parseLocalDateTime(startValue);
  const end = parseLocalDateTime(endValue);
  return Math.round((end.getTime() - start.getTime()) / (60 * 1000));
}

export function formatDurationMinutes(value) {
  const totalMinutes = Math.max(0, Number(value) || 0);
  const days = Math.floor(totalMinutes / (24 * 60));
  const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
  const minutes = totalMinutes % 60;
  const parts = [];

  if (days) {
    parts.push(`${days}天`);
  }
  if (hours) {
    parts.push(`${hours}小时`);
  }
  if (minutes || parts.length === 0) {
    parts.push(`${minutes}分`);
  }

  return parts.join("");
}

export function safeNumber(value) {
  if (value === null || value === undefined || value === "") {
    return null;
  }

  const number = Number(value);
  return Number.isFinite(number) ? number : null;
}


path: scripts/query-onehai-live-chrome.mjs
content: #!/usr/bin/env node
import { execFile as execFileCallback } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { normalizeQuery } from "./query.mjs";
import { splitLocalDateTime, nowIso } from "./utils.mjs";
import { extractPriceSignalsFromText, summarizeSignals } from "./extract.mjs";
import { buildOneHaiPeakRentalHint, parseOneHaiInventoryCount } from "./onehai-policy.mjs";

const execFile = promisify(execFileCallback);

function parseArgs(argv) {
  const args = {};
  for (let index = 0; index < argv.length; index += 1) {
    const part = argv[index];
    if (!part.startsWith("--")) {
      continue;
    }

    const key = part.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
    const next = argv[index + 1];
    if (!next || next.startsWith("--")) {
      args[key] = true;
      continue;
    }

    args[key] = next;
    index += 1;
  }
  return args;
}

function buildAppleScript(url, javascript, delaySeconds = 5) {
  return `
tell application "Google Chrome"
  if (count of windows) is 0 then
    make new window
  end if
  set targetTab to make new tab at end of tabs of front window with properties {URL:${JSON.stringify(url)}}
  delay ${delaySeconds}
  set scriptResult to execute targetTab javascript ${JSON.stringify(javascript)}
  close targetTab
  return scriptResult
end tell
`.trim();
}

async function runAppleScript(script) {
  try {
    const { stdout } = await execFile("osascript", ["-e", script], {
      encoding: "utf8",
      maxBuffer: 1024 * 1024 * 4
    });
    return stdout.trim();
  } catch (error) {
    const output = String(error.stderr || error.stdout || error.message || error);
    if (output.includes("通过 AppleScript 执行 JavaScript 的功能已关闭")) {
      throw new Error("Chrome 当前关闭了“允许 Apple 事件中的 JavaScript”，请先在 Chrome 菜单栏的“查看 > 开发者”中开启它。");
    }
    throw new Error(output.trim());
  }
}

function buildResolveBookingJavaScript(query) {
  const pickupTime = splitLocalDateTime(query.pickupAt);
  const dropoffTime = splitLocalDateTime(query.dropoffAt);

  return `
(function () {
  function appendFormValue(params, key, value) {
    if (value === undefined || value === null) {
      params.append(key, "");
      return;
    }
    if (typeof value === "object" && !Array.isArray(value)) {
      Object.keys(value).forEach(function (childKey) {
        appendFormValue(params, key + "[" + childKey + "]", value[childKey]);
      });
      return;
    }
    if (Array.isArray(value)) {
      value.forEach(function (item) {
        appendFormValue(params, key + "[]", item);
      });
      return;
    }
    params.append(key, String(value));
  }

  function post(url, data) {
    var xhr = new XMLHttpRequest();
    var params = new URLSearchParams();
    if (data && typeof data === "object") {
      Object.keys(data).forEach(function (key) {
        appendFormValue(params, key, data[key]);
      });
    }

    xhr.open("POST", url, false);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    xhr.send(params.toString());

    if (xhr.status < 200 || xhr.status >= 300) {
      throw new Error("HTTP " + xhr.status + " " + xhr.statusText);
    }

    return xhr.responseText;
  }

  function findCity(cityList, cityName) {
    return cityList.find(function (item) {
      return item.cityName === cityName;
    }) || null;
  }

  function pickStore(stores, locationHint) {
    if (!Array.isArray(stores) || stores.length === 0) {
      return null;
    }

    function scoreStore(store, scene, hint) {
      var text = [store.storeName, store.address, store.district].filter(Boolean).join(" ");
      var score = 0;

      if (hint && text.indexOf(hint) >= 0) {
        score += 10000;
      }

      if (scene === "train_station") {
        if (/高铁站/.test(text)) score += 600;
        if (/火车站/.test(text)) score += 520;
        if (/站内取还/.test(text)) score += 360;
        if (/客运/.test(text)) score -= 260;
        if (/机场/.test(text)) score -= 400;
      }

      if (scene === "airport") {
        if (/机场|航站楼|T1|T2|T3/.test(text)) score += 600;
        if (/高铁站|火车站/.test(text)) score -= 400;
      }

      if (/站内取还/.test(text)) score += 80;
      if (/店/.test(store.storeName || "")) score += 30;
      if (/自助点/.test(text)) score += 10;
      if (/停车场/.test(text)) score -= 12;
      if (/送车点/.test(text)) score -= 20;

      return score;
    }

    return stores.slice().sort(function (left, right) {
      return scoreStore(right, queryScene, locationHint) - scoreStore(left, queryScene, locationHint);
    })[0];
  }

  var cityList = JSON.parse(post("https://www.1hai.cn/Home/CityList", null)).data || [];
  var pickupCity = findCity(cityList, ${JSON.stringify(query.pickup.city)});
  var dropoffCity = findCity(cityList, ${JSON.stringify(query.dropoff.city)});
  if (!pickupCity) {
    return JSON.stringify({ ok: false, reason: "一嗨站内城市列表未找到 ${query.pickup.city}。" });
  }
  if (!dropoffCity) {
    return JSON.stringify({ ok: false, reason: "一嗨站内城市列表未找到 ${query.dropoff.city}。" });
  }

  var pickupStores = JSON.parse(post("https://www.1hai.cn/Premises/RegionalStore", { cityId: pickupCity.cityId })).data || [];
  var queryScene = ${JSON.stringify(query.pickup.scene)};
  var pickupStore = pickStore(pickupStores, ${JSON.stringify(query.pickup.location)});
  if (!pickupStore) {
    return JSON.stringify({ ok: false, reason: "${query.pickup.city} 当前未返回可预订门店。" });
  }

  var returnStore = pickupStore;
  if (${JSON.stringify(query.dropoff.city !== query.pickup.city || Boolean(query.dropoff.location))}) {
    var returnStores = JSON.parse(post("https://www.1hai.cn/Premises/RegionalStore", { cityId: dropoffCity.cityId })).data || [];
    queryScene = ${JSON.stringify(query.dropoff.scene)};
    returnStore = pickStore(returnStores, ${JSON.stringify(query.dropoff.location)});
    if (!returnStore) {
      return JSON.stringify({ ok: false, reason: "${query.dropoff.city} 当前未返回可预订还车门店。" });
    }
  }

  var redirectResponse = JSON.parse(post("https://www.1hai.cn/Home/RedirectFirstStep?v=" + Date.now(), {
    pickupDto: {
      pickUpCityId: String(pickupCity.cityId),
      getCarCity: pickupCity.cityName,
      getStoreId: String(pickupStore.id),
      getCarCityMenDian: pickupStore.storeName,
      txtGetCarAddress: "",
      getAddress: "",
      getCheck: false,
      pickUpTime: ${JSON.stringify(pickupTime.date)},
      pickUpHour: ${JSON.stringify(pickupTime.hour)},
      pickUpMinute: ${JSON.stringify(pickupTime.minute)},
      getLng: "",
      getLat: ""
    },
    returnDto: {
      returnCityId: String(dropoffCity.cityId),
      retCarCity: dropoffCity.cityName,
      retStoreId: String(returnStore.id),
      retCarCityMenDian: returnStore.storeName,
      txtDropCarAddress: "",
      retAddress: "",
      retCheck: false,
      returnTime: ${JSON.stringify(dropoffTime.date)},
      retHour: ${JSON.stringify(dropoffTime.hour)},
      returnMinute: ${JSON.stringify(dropoffTime.minute)},
      retLng: "",
      retLat: ""
    }
  }));

  if (!redirectResponse.success || !redirectResponse.data || !redirectResponse.data.redirectUrl) {
    return JSON.stringify({
      ok: false,
      reason: redirectResponse.message || "一嗨未返回选车页跳转地址。"
    });
  }

  return JSON.stringify({
    ok: true,
    bookingUrl: redirectResponse.data.redirectUrl,
    pickupStore: pickupStore.storeName,
    dropoffStore: returnStore.storeName
  });
})()
`.trim();
}

function buildReadBookingPageJavaScript() {
  return `
(function () {
  var vehicleCards = Array.from(document.querySelectorAll(".cartype-list")).map(function (card, index) {
    var canvas = card.querySelector(".cartype-price-current canvas");
    return {
      index: index + 1,
      text: (card.innerText || "").trim(),
      dataUrl: canvas ? canvas.toDataURL("image/png") : null
    };
  }).filter(function (item) {
    return item.dataUrl;
  });

  return JSON.stringify({
    url: location.href,
    title: document.title,
    bodyText: document.body.innerText,
    vehicleCards: vehicleCards
  });
})()
`.trim();
}

async function ocrCanvasDataUrl(dataUrl, outputPath) {
  const base64 = String(dataUrl || "").replace(/^data:image\/png;base64,/, "");
  await fs.writeFile(outputPath, Buffer.from(base64, "base64"));

  const { stdout } = await execFile("tesseract", [
    outputPath,
    "stdout",
    "--psm",
    "8",
    "-c",
    "tessedit_char_whitelist=0123456789"
  ], {
    encoding: "utf8",
    maxBuffer: 1024 * 1024
  });

  const digits = String(stdout || "").replace(/[^\d]/g, "");
  return digits ? Number(digits) : null;
}

function normalizeVehicleName(text) {
  const firstLine = String(text || "")
    .split(/\r?\n/)
    .map((line) => line.trim())
    .find(Boolean);

  if (!firstLine) {
    return null;
  }

  return firstLine.replace(/^(热门上新|长租特惠)\s*/u, "").trim() || null;
}

async function extractVehicleSamples(vehicleCards) {
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "onehai-live-"));
  const samples = [];

  try {
    for (const card of vehicleCards || []) {
      const outputPath = path.join(tempDir, `vehicle-${card.index}.png`);
      const price = await ocrCanvasDataUrl(card.dataUrl, outputPath).catch(() => null);
      if (price === null) {
        continue;
      }

      samples.push({
        index: card.index,
        vehicleName: normalizeVehicleName(card.text),
        priceDailyAverage: price
      });
    }
  } finally {
    await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
  }

  return samples;
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const query = normalizeQuery({
    pickupCity: args.pickupCity,
    pickupLocation: args.pickupLocation,
    pickupScene: args.pickupScene,
    dropoffCity: args.dropoffCity,
    dropoffLocation: args.dropoffLocation,
    dropoffScene: args.dropoffScene,
    pickupDateTime: args.pickupDatetime,
    dropoffDateTime: args.dropoffDatetime,
    vehicleClass: args.vehicleClass
  });

  const bookingRaw = await runAppleScript(buildAppleScript(
    "https://www.1hai.cn/index.aspx",
    buildResolveBookingJavaScript(query),
    5
  ));
  const booking = JSON.parse(bookingRaw);
  if (!booking.ok) {
    console.log(JSON.stringify({
      platform: "onehai-live-chrome",
      status: "fallback",
      capturedAt: nowIso(),
      query,
      warnings: [booking.reason]
    }, null, 2));
    return;
  }

  const bookingPageRaw = await runAppleScript(buildAppleScript(
    booking.bookingUrl,
    buildReadBookingPageJavaScript(),
    8
  ));
  const bookingPage = JSON.parse(bookingPageRaw);
  const inventoryCount = parseOneHaiInventoryCount(bookingPage.bodyText);
  const signals = extractPriceSignalsFromText(bookingPage.bodyText).filter((item) => {
    return !/元以下|元以上|\d+\s*-\s*\d+\s*元/.test(item.snippet);
  });
  const summary = summarizeSignals(signals);
  const requiresLogin = bookingPage.bodyText.includes("登录账户，即可查看库存与价格");
  const noCarsAvailable = inventoryCount === 0;
  const peakRentalHint = noCarsAvailable ? buildOneHaiPeakRentalHint(query, bookingPage.bodyText) : null;
  const vehicleSamples = noCarsAvailable ? [] : await extractVehicleSamples(bookingPage.vehicleCards);
  const priceMinFromVehicles = vehicleSamples.length
    ? Math.min(...vehicleSamples.map((item) => item.priceDailyAverage))
    : null;

  console.log(JSON.stringify({
    platform: "onehai-live-chrome",
    status: !requiresLogin && !noCarsAvailable && (summary.priceMin || priceMinFromVehicles) ? "priced" : "fallback",
    capturedAt: nowIso(),
    query,
    bookingUrl: booking.bookingUrl,
    selectedStore: {
      pickup: booking.pickupStore,
      dropoff: booking.dropoffStore
    },
    authMode: "live_chrome",
    priceMin: !noCarsAvailable ? (summary.priceMin ?? priceMinFromVehicles) : null,
    priceTotalIfAvailable: !noCarsAvailable ? (summary.priceTotalIfAvailable ?? priceMinFromVehicles) : null,
    availableCars: noCarsAvailable ? 0 : (inventoryCount ?? summary.availableCars),
    pricingUnit: !noCarsAvailable && (summary.pricingUnit || vehicleSamples.length) ? "day" : null,
    vehicleClass: summary.vehicleClass || query.vehicleClass,
    vehicleSamples,
    bookingRestriction: peakRentalHint,
    warnings: [
      ...(requiresLogin ? ["当前 Chrome 会话打开的一嗨选车页仍提示需要登录。"] : []),
      ...(noCarsAvailable ? [peakRentalHint?.warning || "当前选择的门店与时段暂无可租车型。"] : [])
    ],
    preview: bookingPage.bodyText.slice(0, 1200)
  }, null, 2));
}

main().catch((error) => {
  console.error(error.message || error);
  process.exitCode = 1;
});


path: scripts/onehai-policy.mjs
content: import { addDaysToLocalDateTime, formatDurationMinutes, getLocalDurationMinutes } from "./utils.mjs";

const TRAIN_SCENE_ALIASES = new Set(["train", "rail", "railway", "station", "train_station"]);
const AIRPORT_SCENE_ALIASES = new Set(["airport", "flight"]);
const CITY_SCENE_ALIASES = new Set(["city", "downtown", "urban"]);

export function inferTransitSceneFromLocation(location) {
  const text = String(location || "").trim();
  if (!text) {
    return null;
  }

  if (/(高铁站|火车站|站内取还)/u.test(text)) {
    return "train_station";
  }

  if (/(机场|航站楼|T1|T2|T3)/u.test(text)) {
    return "airport";
  }

  return null;
}

export function normalizeTransitScene(value, locationHint = null) {
  const raw = String(value || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
  if (!raw) {
    return inferTransitSceneFromLocation(locationHint);
  }

  if (TRAIN_SCENE_ALIASES.has(raw)) {
    return "train_station";
  }

  if (AIRPORT_SCENE_ALIASES.has(raw)) {
    return "airport";
  }

  if (CITY_SCENE_ALIASES.has(raw)) {
    return "city";
  }

  return inferTransitSceneFromLocation(locationHint);
}

export function parseOneHaiInventoryCount(text) {
  const match = String(text || "").match(/共有\s*(\d+)\s*种车型可以租用/u);
  if (!match) {
    return null;
  }

  return Number(match[1]);
}

export function parseOneHaiPeakRentalRule(text) {
  const source = String(text || "");
  const minimumDaysMatch = source.match(/租期延长至\s*(\d+)\s*天以上/u);
  if (!minimumDaysMatch) {
    return null;
  }

  const peakPeriodMatch = source.match(/(\d{4}年\d{2}月\d{2}日)\s*-\s*(\d{4}年\d{2}月\d{2}日)\s*为用车高峰期/u);
  return {
    minimumDays: Number(minimumDaysMatch[1]),
    peakStartDate: peakPeriodMatch?.[1] || null,
    peakEndDate: peakPeriodMatch?.[2] || null
  };
}

export function buildOneHaiPeakRentalHint(query, text) {
  const rule = parseOneHaiPeakRentalRule(text);
  if (!rule) {
    return null;
  }

  const currentDurationMinutes = getLocalDurationMinutes(query.pickupAt, query.dropoffAt);
  const minimumDurationMinutes = rule.minimumDays * 24 * 60;
  const periodPrefix = rule.peakStartDate && rule.peakEndDate
    ? `${rule.peakStartDate}-${rule.peakEndDate} 为用车高峰期，`
    : "当前是用车高峰期，";

  if (currentDurationMinutes < minimumDurationMinutes) {
    const suggestedDropoffAt = addDaysToLocalDateTime(query.pickupAt, rule.minimumDays);
    return {
      rule,
      suggestedDropoffAt,
      warning: `${periodPrefix}至少需租满 ${rule.minimumDays} 天；当前仅 ${formatDurationMinutes(currentDurationMinutes)}，建议还车时间不早于 ${suggestedDropoffAt}。`
    };
  }

  return {
    rule,
    suggestedDropoffAt: null,
    warning: `${periodPrefix}平台提示建议租满 ${rule.minimumDays} 天以上。`
  };
}


path: scripts/extract.mjs
content: function toPriceNumber(raw) {
  const value = Number(String(raw).replace(/,/g, ""));
  if (!Number.isFinite(value) || value < 20 || value > 50000) {
    return null;
  }

  return Math.round(value * 100) / 100;
}

export function extractPriceSignalsFromText(text) {
  const source = String(text || "").replace(/\u00a0/g, " ");
  const results = [];
  const patterns = [
    { regex: /(?:¥|￥|RMB|CNY)\s*([0-9]{2,5}(?:\.[0-9]{1,2})?)/gi, unit: "unknown" },
    { regex: /([0-9]{2,5}(?:\.[0-9]{1,2})?)\s*元\s*(起|\/天|每天|\/日)?/gi, unit: "unknown" }
  ];

  for (const pattern of patterns) {
    let match;
    while ((match = pattern.regex.exec(source)) !== null) {
      const price = toPriceNumber(match[1]);
      if (!price) {
        continue;
      }

      const snippet = source.slice(Math.max(0, match.index - 40), Math.min(source.length, match.index + 80));
      results.push({
        price,
        snippet: snippet.trim(),
        pricingUnit: /\/天|每天|\/日/.test(match[0]) ? "day" : "unknown"
      });
    }
  }

  return dedupePriceSignals(results);
}

export function dedupePriceSignals(items) {
  const seen = new Set();
  const deduped = [];
  for (const item of items) {
    const key = `${item.price}:${item.pricingUnit}:${item.snippet}`;
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    deduped.push(item);
  }
  return deduped.sort((a, b) => a.price - b.price);
}

export function summarizeSignals(signals) {
  if (!signals.length) {
    return {
      priceMin: null,
      priceTotalIfAvailable: null,
      availableCars: 0,
      vehicleClass: null,
      pricingUnit: null
    };
  }

  const sortedSignals = [...signals].sort((a, b) => a.price - b.price);

  return {
    priceMin: sortedSignals[0].price,
    priceTotalIfAvailable: sortedSignals.find((item) => item.pricingUnit === "unknown")?.price ?? sortedSignals[0].price,
    availableCars: sortedSignals.length,
    vehicleClass: guessVehicleClass(sortedSignals),
    pricingUnit: sortedSignals[0].pricingUnit
  };
}

function guessVehicleClass(signals) {
  const snippet = signals.map((item) => item.snippet).join(" ");
  const labels = [
    "经济型",
    "舒适型",
    "紧凑型",
    "SUV",
    "商务",
    "MPV",
    "豪华型",
    "中大型"
  ];

  return labels.find((label) => snippet.includes(label)) || null;
}


path: agents/openai.yaml
content: interface:
  display_name: "China Rental Price"
  short_description: "中国境内租车参考价查询与门店场景优先判断"
  default_prompt: "Use $china-rental-price to query a China domestic rental-car reference price, prefer train-station or airport stores, and flag holiday minimum-rental rules."

policy:
  allow_implicit_invocation: true


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)