jackwener/OpenCLI · error · ArgumentError

<train-no> "${trainNo}" does not look like a 12306 internal

Error message

<train-no> "${trainNo}" does not look like a 12306 internal train_no

What it means

The `12306 price` command requires the internal 12306 `train_no` (e.g. 24000000G10L), an 8-18 char alphanumeric identifier returned as the `train_no` field of `12306 trains`, not the public train code like G1. The library throws this ArgumentError when the value passed to `--train-no` fails the TRAIN_NO_RE pattern (/^[0-9A-Za-z]{8,18}$/). It exists to fail fast before making network calls with an identifier the upstream API would reject.

Source

Thrown at clis/12306/price.js:129

    name: 'price',
    access: 'read',
    description: 'Look up 12306 ticket prices by seat class for one train on a given date and segment (anonymous, no login required)',
    domain: 'kyfw.12306.cn',
    strategy: Strategy.PUBLIC,
    browser: false,
    args: [
        { name: 'train-no', positional: true, required: true, help: 'Internal train_no from `12306 trains` (e.g. 24000000G10L)' },
        { name: 'from', required: true, help: 'Origin station (Chinese name, telecode, or pinyin) - must be a stop of this train' },
        { name: 'to', required: true, help: 'Destination station - must be a stop of this train' },
        { name: 'date', required: true, help: 'Departure date in YYYY-MM-DD' },
        { name: 'seat-types', default: 'OM9PA1A3A4FWZ', help: 'Seat-type letters to query (default covers the common classes). Examples: OM9 (二等/一等/商务), A1A3A4 (硬座/硬卧/软卧).' },
    ],
    columns: ['seat_code', 'seat_name', 'price', 'currency'],
    func: async (kwargs) => {
        const trainNo = String(kwargs['train-no'] ?? '').trim();
        if (!trainNo) throw new ArgumentError('<train-no> must not be empty');
        if (!TRAIN_NO_RE.test(trainNo)) {
            throw new ArgumentError(
                `<train-no> "${trainNo}" does not look like a 12306 internal train_no`,
                'Use the train_no field from `12306 trains` output (e.g. 24000000G10L), not the public code (G1).',
            );
        }
        const fromArg = String(kwargs.from ?? '').trim();
        const toArg = String(kwargs.to ?? '').trim();
        if (!fromArg) throw new ArgumentError('--from station must not be empty');
        if (!toArg) throw new ArgumentError('--to station must not be empty');
        const date = validateDate(kwargs.date);
        const seatTypes = String(kwargs['seat-types'] ?? '').trim() || 'OM9PA1A3A4FWZ';
        if (!SEAT_TYPES_RE.test(seatTypes)) {
            throw new ArgumentError('--seat-types must contain only 12306 seat letters/digits (A-Z, 0-9)');
        }

        const stations = await fetchStationBundle();
        const fromStation = resolveStation(stations, fromArg);
        const toStation = resolveStation(stations, toArg);
        if (fromStation.code === toStation.code) {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Run `12306 trains` first and copy the `train_no` field value (e.g. 24000000G10L), not the public code.
  2. Check the value is 8-18 alphanumeric characters with no whitespace, quotes, or hyphens: /^[0-9A-Za-z]{8,18}$/.
  3. If scripting, extract train_no programmatically from `12306 trains --json` output rather than hardcoding.

Example fix

// before
12306 price --train-no G1 --from 北京南 --to 上海
// after
12306 trains --from 北京南 --to 上海   # take train_no, e.g. 24000000G10L
12306 price --train-no 24000000G10L --from 北京南 --to 上海
Defensive patterns

Strategy: validation

Validate before calling

const trainNo = String(opts['train-no'] ?? '').trim();
if (!/^[0-9A-Za-z]{8,18}$/.test(trainNo)) {
  throw new Error(`--train-no must be the internal train_no from '12306 trains' (8-18 alphanumerics), got: ${trainNo}`);
}

Type guard

const isValidTrainNo = (v) => typeof v === 'string' && /^[0-9A-Za-z]{8,18}$/.test(v);

Try / catch

try {
  const rows = await price({ 'train-no': tn, from, to, date });
} catch (e) {
  if (/does not look like a 12306 internal train_no/.test(e.message)) {
    const trains = await trainsCmd({ from, to, date });
    tn = trains[0].train_no; // recover by looking up the real train_no
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `12306 price --train-no G1 --from ... --to ...` where train-no is empty after trimming, too short/long (not 8-18 chars), or contains characters outside A-Z a-z 0-9 — typically because the public train code (G1, K599) was passed instead of the internal train_no.

Common situations: Developers copy the public train code from a timetable or ticket instead of the `train_no` column from `12306 trains` output; quoting/whitespace issues that leave stray characters; piping the wrong CSV/JSON field into the CLI.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/07de5a402a5dfbb4. Report an issue: GitHub.