{"record":{"id":"07de5a402a5dfbb4","repo":"jackwener/OpenCLI","slug":"train-no-trainno-does-not-look-like-a-12306","errorCode":null,"errorMessage":"<train-no> \"${trainNo}\" does not look like a 12306 internal train_no","messagePattern":"<train-no> \"(.+?)\" does not look like a 12306 internal train_no","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/12306/price.js","lineNumber":129,"sourceCode":"    name: 'price',\n    access: 'read',\n    description: 'Look up 12306 ticket prices by seat class for one train on a given date and segment (anonymous, no login required)',\n    domain: 'kyfw.12306.cn',\n    strategy: Strategy.PUBLIC,\n    browser: false,\n    args: [\n        { name: 'train-no', positional: true, required: true, help: 'Internal train_no from `12306 trains` (e.g. 24000000G10L)' },\n        { name: 'from', required: true, help: 'Origin station (Chinese name, telecode, or pinyin) - must be a stop of this train' },\n        { name: 'to', required: true, help: 'Destination station - must be a stop of this train' },\n        { name: 'date', required: true, help: 'Departure date in YYYY-MM-DD' },\n        { name: 'seat-types', default: 'OM9PA1A3A4FWZ', help: 'Seat-type letters to query (default covers the common classes). Examples: OM9 (二等/一等/商务), A1A3A4 (硬座/硬卧/软卧).' },\n    ],\n    columns: ['seat_code', 'seat_name', 'price', 'currency'],\n    func: async (kwargs) => {\n        const trainNo = String(kwargs['train-no'] ?? '').trim();\n        if (!trainNo) throw new ArgumentError('<train-no> must not be empty');\n        if (!TRAIN_NO_RE.test(trainNo)) {\n            throw new ArgumentError(\n                `<train-no> \"${trainNo}\" does not look like a 12306 internal train_no`,\n                'Use the train_no field from `12306 trains` output (e.g. 24000000G10L), not the public code (G1).',\n            );\n        }\n        const fromArg = String(kwargs.from ?? '').trim();\n        const toArg = String(kwargs.to ?? '').trim();\n        if (!fromArg) throw new ArgumentError('--from station must not be empty');\n        if (!toArg) throw new ArgumentError('--to station must not be empty');\n        const date = validateDate(kwargs.date);\n        const seatTypes = String(kwargs['seat-types'] ?? '').trim() || 'OM9PA1A3A4FWZ';\n        if (!SEAT_TYPES_RE.test(seatTypes)) {\n            throw new ArgumentError('--seat-types must contain only 12306 seat letters/digits (A-Z, 0-9)');\n        }\n\n        const stations = await fetchStationBundle();\n        const fromStation = resolveStation(stations, fromArg);\n        const toStation = resolveStation(stations, toArg);\n        if (fromStation.code === toStation.code) {","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/12306/price.js#L111-L147","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Run `12306 trains` first and copy the `train_no` field value (e.g. 24000000G10L), not the public code.","Check the value is 8-18 alphanumeric characters with no whitespace, quotes, or hyphens: /^[0-9A-Za-z]{8,18}$/.","If scripting, extract train_no programmatically from `12306 trains --json` output rather than hardcoding."],"exampleFix":"// before\n12306 price --train-no G1 --from 北京南 --to 上海\n// after\n12306 trains --from 北京南 --to 上海   # take train_no, e.g. 24000000G10L\n12306 price --train-no 24000000G10L --from 北京南 --to 上海","handlingStrategy":"validation","validationCode":"const trainNo = String(opts['train-no'] ?? '').trim();\nif (!/^[0-9A-Za-z]{8,18}$/.test(trainNo)) {\n  throw new Error(`--train-no must be the internal train_no from '12306 trains' (8-18 alphanumerics), got: ${trainNo}`);\n}","typeGuard":"const isValidTrainNo = (v) => typeof v === 'string' && /^[0-9A-Za-z]{8,18}$/.test(v);","tryCatchPattern":"try {\n  const rows = await price({ 'train-no': tn, from, to, date });\n} catch (e) {\n  if (/does not look like a 12306 internal train_no/.test(e.message)) {\n    const trains = await trainsCmd({ from, to, date });\n    tn = trains[0].train_no; // recover by looking up the real train_no\n  } else throw e;\n}","preventionTips":["Always source train_no from the `12306 trains` output, never the public code.","Validate with /^[0-9A-Za-z]{8,18}$/ before calling.","In scripts, parse train_no programmatically from `12306 trains --json` instead of hardcoding."],"tags":["argument-validation","cli","input-format"],"backgroundTag":"invalid-argument-format","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}