{"record":{"id":"f7e6b9020d10c9c3","repo":"jackwener/OpenCLI","slug":"seat-types-must-contain-only-12306-seat-letters","errorCode":null,"errorMessage":"--seat-types must contain only 12306 seat letters/digits (A-Z, 0-9)","messagePattern":"--seat-types must contain only 12306 seat letters/digits \\(A-Z, 0-9\\)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/12306/price.js","lineNumber":141,"sourceCode":"    ],\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) {\n            throw new ArgumentError(`--from and --to must differ; both resolved to ${fromStation.name} (${fromStation.code})`);\n        }\n\n        const cookieHeader = await mintSession();\n        const stops = await queryStopsForPrice(cookieHeader, trainNo, fromStation.code, toStation.code, date);\n        const { fromNo, toNo } = pickStationNos(stops, fromStation.code, toStation.code, fromStation.name, toStation.name);\n        const priceData = await queryPrice(cookieHeader, trainNo, fromNo, toNo, seatTypes, date);\n        const rows = parsePriceData(priceData);\n        if (rows.length === 0) {\n            throw new EmptyResultError(\n                `No prices returned for train_no=${trainNo} ${fromStation.name} -> ${toStation.name} on ${date}`,\n                'Try a different seat-types letter set, or check that this train operates on the date.',","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/12306/price.js#L123-L159","documentation":"The `--seat-types` option must be a string of 1-32 uppercase letters/digits matching SEAT_TYPES_RE (/^[A-Z0-9]{1,32}$/), because 12306's price endpoint keys prices by seat-type letter codes (O=二等座, M=一等座, A9=商务座, WZ=无座, etc.). The library throws this ArgumentError when the supplied value contains lowercase letters or other characters. Empty values are fine — they default to 'OM9PA1A4FWZ' style defaults.","triggerScenarios":"Passing `--seat-types \"o,m\"` (lowercase/commas), `--seat-types 二等座` (Chinese text), or any value with symbols/spaces to `12306 price`.","commonSituations":"Users pass human-readable seat names instead of letter codes; lowercase seat letters copied from other tools; commas or spaces separating multiple seat types.","solutions":["Omit --seat-types entirely to use the default set 'OM9PA1A3A4FWZ'.","Use uppercase letter codes only: e.g. --seat-types OM9 (O=二等座, M=一等座, 9/A9=商务座).","Uppercase and strip separators before passing: value.toUpperCase().replace(/[^A-Z0-9]/g, '')."],"exampleFix":"// before\n12306 price --train-no 24000000G10L --from 北京南 --to 上海 --seat-types \"o, m\"\n// after\n12306 price --train-no 24000000G10L --from 北京南 --to 上海 --seat-types OM   # O=二等座 M=一等座","handlingStrategy":"validation","validationCode":"const seatTypes = String(opts['seat-types'] ?? '').trim().toUpperCase().replace(/[^A-Z0-9]/g, '');\nif (seatTypes && !/^[A-Z0-9]{1,32}$/.test(seatTypes)) {\n  throw new Error('--seat-types must be uppercase letter codes only (e.g. OM9), or omitted for the default');\n}","typeGuard":"const isValidSeatTypes = (v) => typeof v === 'string' && /^[A-Z0-9]{1,32}$/.test(v);","tryCatchPattern":"try {\n  await price({ 'train-no': tn, from, to, date, 'seat-types': st });\n} catch (e) {\n  if (/--seat-types must contain only/.test(e.message)) {\n    // drop the option and use the library default\n    await price({ 'train-no': tn, from, to, date });\n  } else throw e;\n}","preventionTips":["Omit --seat-types unless you specifically need a subset of seat classes.","Use uppercase letter codes (O, M, A9, A1, A3, A4, F, P, WZ), never seat names or lowercase.","Sanitize by uppercasing and stripping non-alphanumerics before passing."],"tags":["argument-validation","cli","input-format"],"backgroundTag":"invalid-argument-format","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}