新增插件: - terminal: 持久交互式终端,纯标准库,全平台,13个测试 - operit_bridge: Operit AI工具格式桥接,47个脚本288个工具,12个测试 其他: - plugins/operit/: 47个Operit官方脚本 - 更新plugin_registry.json/plugins_config.py - 更新about.txt/CHANGELOG.md至v5.4.2
1015 lines
53 KiB
TypeScript
1015 lines
53 KiB
TypeScript
/* METADATA
|
|
{
|
|
"name": "12306_ticket",
|
|
|
|
"display_name": {
|
|
"zh": "12306 拓展",
|
|
"en": "12306 Extension"
|
|
},
|
|
"description": { "zh": "提供12306火车票信息查询功能,包括余票、中转、经停站等。", "en": "Query China Railway 12306 train ticket information, including availability, transfer routes, and stop stations." },
|
|
"enabledByDefault": true,
|
|
"category": "Life",
|
|
"tools": [
|
|
{
|
|
"name": "get_current_date",
|
|
"description": { "zh": "获取当前日期,以上海时区(Asia/Shanghai, UTC+8)为准,返回格式为 'yyyy-MM-dd'。主要用于解析用户提到的相对日期(如“明天”、“下周三”),为其他需要日期的接口提供准确的日期输入。", "en": "Get the current date in the Shanghai timezone (Asia/Shanghai, UTC+8). Returns format 'yyyy-MM-dd'. Mainly used to resolve relative dates (e.g. \"tomorrow\", \"next Wednesday\") and provide correct date input for other APIs." },
|
|
"parameters": []
|
|
},
|
|
{
|
|
"name": "get_stations_code_in_city",
|
|
"description": { "zh": "通过中文城市名查询该城市 **所有** 火车站的名称及其对应的 `station_code`,结果是一个包含多个车站信息的列表。", "en": "Given a Chinese city name, list **all** train stations in that city and their corresponding `station_code`." },
|
|
"parameters": [
|
|
{ "name": "city", "description": { "zh": "中文城市名称,例如:'北京', '上海'", "en": "Chinese city name, e.g. '北京', '上海'." }, "type": "string", "required": true }
|
|
]
|
|
},
|
|
{
|
|
"name": "get_station_code_of_citys",
|
|
"description": { "zh": "通过中文城市名查询代表该城市的 `station_code`。此接口主要用于在用户提供**城市名**作为出发地或到达地时,为接口准备 `station_code` 参数。", "en": "Get the representative `station_code` for a Chinese city name. Use this when the user provides a **city name** as origin/destination and you need a `station_code`." },
|
|
"parameters": [
|
|
{ "name": "citys", "description": { "zh": "要查询的城市,比如'北京'。若要查询多个城市,请用|分割,比如'北京|上海'。", "en": "City to query, e.g. '北京'. For multiple cities, separate with |, e.g. '北京|上海'." }, "type": "string", "required": true }
|
|
]
|
|
},
|
|
{
|
|
"name": "get_station_code_by_names",
|
|
"description": { "zh": "通过具体的中文车站名查询其 `station_code` 和车站名。此接口主要用于在用户提供**具体车站名**作为出发地或到达地时,为接口准备 `station_code` 参数。", "en": "Given a specific Chinese station name, return its `station_code` and station name. Use this when the user provides a **specific station name** for origin/destination." },
|
|
"parameters": [
|
|
{ "name": "station_names", "description": { "zh": "具体的中文车站名称,例如:'北京南', '上海虹桥'。若要查询多个站点,请用|分割,比如'北京南|上海虹桥'。", "en": "Specific Chinese station names, e.g. '北京南', '上海虹桥'. For multiple stations, separate with |, e.g. '北京南|上海虹桥'." }, "type": "string", "required": true }
|
|
]
|
|
},
|
|
{
|
|
"name": "get_station_by_telecode",
|
|
"description": { "zh": "通过车站的 `station_telecode` 查询车站的详细信息,包括名称、拼音、所属城市等。此接口主要用于在已知 `telecode` 的情况下获取更完整的车站数据,或用于特殊查询及调试目的。一般用户对话流程中较少直接触发。", "en": "Query station details by `station_telecode`, including name, pinyin, city, etc. Mainly for getting more complete station data when `telecode` is known, or for special queries/debugging." },
|
|
"parameters": [
|
|
{ "name": "station_telecode", "description": { "zh": "车站的 `station_telecode` (3位字母编码)", "en": "Station `station_telecode` (3-letter code)." }, "type": "string", "required": true }
|
|
]
|
|
},
|
|
{
|
|
"name": "get_tickets",
|
|
"description": { "zh": "查询12306余票信息。", "en": "Query 12306 ticket availability." },
|
|
"parameters": [
|
|
{ "name": "date", "description": { "zh": "查询日期,格式为 'yyyy-MM-dd'。如果用户提供的是相对日期(如“明天”),请务必先调用 `get_current_date` 接口获取当前日期,并计算出目标日期。", "en": "Query date in 'yyyy-MM-dd'. If the user gives a relative date (e.g. \"tomorrow\"), call `get_current_date` first and compute the target date." }, "type": "string", "required": true },
|
|
{ "name": "from_station", "description": { "zh": "出发地的 `station_code` 。必须是通过 `get_station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Origin `station_code`. Must be obtained via `get_station_code_by_names` or `get_station_code_of_citys` (do NOT pass Chinese names directly)." }, "type": "string", "required": true },
|
|
{ "name": "to_station", "description": { "zh": "到达地的 `station_code` 。必须是通过 `get_station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Destination `station_code`. Must be obtained via `get_station_code_by_names` or `get_station_code_of_citys` (do NOT pass Chinese names directly)." }, "type": "string", "required": true },
|
|
{ "name": "train_filter_flags", "description": { "zh": "车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 'G'。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]", "en": "Train filter flags. Default empty (no filter). Can combine multiple flags. Example: for high-speed rail, use 'G'. Options: [G(High-speed/Intercity),D(EMU),Z(Direct express),T(Express),K(Fast),O(Other),F(Fuxing),S(Smart EMU)]." }, "type": "string", "required": false },
|
|
{ "name": "sort_flag", "description": { "zh": "排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]", "en": "Sort mode. Default empty (no sorting). Only one mode is supported. Options: [startTime (earliest departure), arriveTime (earliest arrival), duration (shortest duration)]." }, "type": "string", "required": false },
|
|
{ "name": "sort_reverse", "description": { "zh": "是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。", "en": "Reverse sort order (default: false). Only effective when sort_flag is set." }, "type": "boolean", "required": false },
|
|
{ "name": "limited_num", "description": { "zh": "返回的余票数量限制,默认为0,即不限制。", "en": "Limit number of returned results (default: 0, no limit)." }, "type": "number", "required": false }
|
|
]
|
|
},
|
|
{
|
|
"name": "get_interline_tickets",
|
|
"description": { "zh": "查询12306中转余票信息。尚且只支持查询前十条。", "en": "Query 12306 transfer (interline) ticket availability. Currently only supports the first 10 results." },
|
|
"parameters": [
|
|
{ "name": "date", "description": { "zh": "查询日期,格式为 'yyyy-MM-dd'。如果用户提供的是相对日期(如“明天”),请务必先调用 `get_current_date` 接口获取当前日期,并计算出目标日期。", "en": "Query date in 'yyyy-MM-dd'. If the user gives a relative date, call `get_current_date` first and compute the target date." }, "type": "string", "required": true },
|
|
{ "name": "from_station", "description": { "zh": "出发地的 `station_code` 。必须是通过 `get-station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Origin `station_code`. Must be obtained via station-code lookup APIs (do NOT pass Chinese names directly)." }, "type": "string", "required": true },
|
|
{ "name": "to_station", "description": { "zh": "到达地的 `station_code` 。必须是通过 `get_station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Destination `station_code`. Must be obtained via station-code lookup APIs (do NOT pass Chinese names directly)." }, "type": "string", "required": true },
|
|
{ "name": "middle_station", "description": { "zh": "中转地的 `station_code` ,可选。必须是通过 `get-station-code-by-names` 或 `get-station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Optional transfer station `station_code`. Must be obtained via station-code lookup APIs (do NOT pass Chinese names directly)." }, "type": "string", "required": false },
|
|
{ "name": "show_wz", "description": { "zh": "是否显示无座车,默认不显示无座车。", "en": "Whether to include no-seat (无座) tickets (default: false)." }, "type": "boolean", "required": false },
|
|
{ "name": "train_filter_flags", "description": { "zh": "车次筛选条件,默认为空。从以下标志中选取多个条件组合[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]", "en": "Train filter flags. Default empty. Combine multiple flags from: [G(High-speed/Intercity),D(EMU),Z(Direct express),T(Express),K(Fast),O(Other),F(Fuxing),S(Smart EMU)]." }, "type": "string", "required": false },
|
|
{ "name": "sort_flag", "description": { "zh": "排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]", "en": "Sort mode. Default empty. Options: startTime / arriveTime / duration." }, "type": "string", "required": false },
|
|
{ "name": "sort_reverse", "description": { "zh": "是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。", "en": "Reverse sort order (default: false). Only effective when sort_flag is set." }, "type": "boolean", "required": false },
|
|
{ "name": "limited_num", "description": { "zh": "返回的中转余票数量限制,默认为10。", "en": "Limit number of returned results (default: 10)." }, "type": "number", "required": false }
|
|
]
|
|
},
|
|
{
|
|
"name": "get_train_route_stations",
|
|
"description": { "zh": "查询特定列车车次在指定区间内的途径车站、到站时间、出发时间及停留时间等详细经停信息。当用户询问某趟具体列车的经停站时使用此接口。", "en": "Query detailed stop information for a specific train within a segment, including stations, arrival/departure times, and stop duration. Use when the user asks for stops of a specific train." },
|
|
"parameters": [
|
|
{ "name": "train_no", "description": { "zh": "要查询的实际车次编号 `train_no`,例如 '240000G10336',而非'G1033'。此编号通常可以从 `get_tickets` 的查询结果中获取,或者由用户直接提供。", "en": "Actual train number `train_no`, e.g. '240000G10336' (not 'G1033'). Usually obtained from `get_tickets` results or provided by the user." }, "type": "string", "required": true },
|
|
{ "name": "from_station_telecode", "description": { "zh": "该列车行程的**出发站**的 `station_telecode` (3位字母编码`)。通常来自 `get_tickets` 结果中的 `telecode` 字段,或者通过 `get_station_code_by_names` 得到。", "en": "`station_telecode` (3-letter code) of the **origin station**. Usually from `telecode` fields in `get_tickets`, or obtained via station code lookup." }, "type": "string", "required": true },
|
|
{ "name": "to_station_telecode", "description": { "zh": "该列车行程的**到达站**的 `station_telecode` (3位字母编码)。通常来自 `get_tickets` 结果中的 `telecode` 字段,或者通过 `get-station_code_by_names` 得到。", "en": "`station_telecode` (3-letter code) of the **destination station**. Usually from `telecode` fields in `get_tickets`, or obtained via station code lookup." }, "type": "string", "required": true },
|
|
{ "name": "depart_date", "description": { "zh": "列车从 `from_station_telecode` 指定的车站出发的日期 (格式: yyyy-MM-dd)。如果用户提供的是相对日期,请务必先调用 `get_current_date` 解析。", "en": "Departure date from the origin station (format: yyyy-MM-dd). If the user provides a relative date, resolve it via `get_current_date`." }, "type": "string", "required": true }
|
|
]
|
|
}
|
|
]
|
|
}*/
|
|
|
|
// #region 类型定义
|
|
type TicketData = {
|
|
secret_Sstr: string;
|
|
button_text_info: string;
|
|
train_no: string;
|
|
station_train_code: string;
|
|
start_station_telecode: string;
|
|
end_station_telecode: string;
|
|
from_station_telecode: string;
|
|
to_station_telecode: string;
|
|
start_time: string;
|
|
arrive_time: string;
|
|
lishi: string;
|
|
canWebBuy: string;
|
|
yp_info: string;
|
|
start_train_date: string;
|
|
train_seat_feature: string;
|
|
location_code: string;
|
|
from_station_no: string;
|
|
to_station_no: string;
|
|
is_support_card: string;
|
|
controlled_train_flag: string;
|
|
gg_num: string;
|
|
gr_num: string;
|
|
qt_num: string;
|
|
rw_num: string;
|
|
rz_num: string;
|
|
tz_num: string;
|
|
wz_num: string;
|
|
yb_num: string;
|
|
yw_num: string;
|
|
yz_num: string;
|
|
ze_num: string;
|
|
zy_num: string;
|
|
swz_num: string;
|
|
srrb_num: string;
|
|
yp_ex: string;
|
|
seat_types: string;
|
|
exchange_train_flag: string;
|
|
houbu_train_flag: string;
|
|
houbu_seat_limit: string;
|
|
yp_info_new: string;
|
|
'40': string;
|
|
'41': string;
|
|
'42': string;
|
|
'43': string;
|
|
'44': string;
|
|
'45': string;
|
|
dw_flag: string;
|
|
'47': string;
|
|
stopcheckTime: string;
|
|
country_flag: string;
|
|
local_arrive_time: string;
|
|
local_start_time: string;
|
|
'52': string;
|
|
bed_level_info: string;
|
|
seat_discount_info: string;
|
|
sale_time: string;
|
|
'56': string;
|
|
};
|
|
|
|
const TicketDataKeys: (keyof TicketData)[] = [
|
|
'secret_Sstr', 'button_text_info', 'train_no', 'station_train_code', 'start_station_telecode',
|
|
'end_station_telecode', 'from_station_telecode', 'to_station_telecode', 'start_time', 'arrive_time',
|
|
'lishi', 'canWebBuy', 'yp_info', 'start_train_date', 'train_seat_feature',
|
|
'location_code', 'from_station_no', 'to_station_no', 'is_support_card', 'controlled_train_flag',
|
|
'gg_num', 'gr_num', 'qt_num', 'rw_num', 'rz_num',
|
|
'tz_num', 'wz_num', 'yb_num', 'yw_num', 'yz_num',
|
|
'ze_num', 'zy_num', 'swz_num', 'srrb_num', 'yp_ex',
|
|
'seat_types', 'exchange_train_flag', 'houbu_train_flag', 'houbu_seat_limit', 'yp_info_new',
|
|
'40', '41', '42', '43', '44',
|
|
'45', 'dw_flag', '47', 'stopcheckTime', 'country_flag',
|
|
'local_arrive_time', 'local_start_time', '52', 'bed_level_info', 'seat_discount_info',
|
|
'sale_time', '56',
|
|
];
|
|
|
|
type TicketInfo = {
|
|
train_no: string;
|
|
start_train_code: string;
|
|
start_date: string;
|
|
start_time: string;
|
|
arrive_date: string;
|
|
arrive_time: string;
|
|
lishi: string;
|
|
from_station: string;
|
|
to_station: string;
|
|
from_station_telecode: string;
|
|
to_station_telecode: string;
|
|
prices: Price[];
|
|
dw_flag: string[];
|
|
};
|
|
|
|
type StationData = {
|
|
station_id: string;
|
|
station_name: string;
|
|
station_code: string;
|
|
station_pinyin: string;
|
|
station_short: string;
|
|
station_index: string;
|
|
code: string;
|
|
city: string;
|
|
r1: string;
|
|
r2: string;
|
|
};
|
|
|
|
const StationDataKeys: (keyof StationData)[] = [
|
|
'station_id', 'station_name', 'station_code', 'station_pinyin', 'station_short',
|
|
'station_index', 'code', 'city', 'r1', 'r2',
|
|
];
|
|
|
|
interface Price {
|
|
seat_name: string;
|
|
short: string;
|
|
seat_type_code: string;
|
|
num: string;
|
|
price: number;
|
|
discount: number | undefined;
|
|
}
|
|
|
|
type RouteStationData = {
|
|
arrive_time: string;
|
|
station_name: string;
|
|
isChina: string;
|
|
start_time: string;
|
|
stopover_time: string;
|
|
station_no: string;
|
|
country_code: string;
|
|
country_name: string;
|
|
isEnabled: boolean;
|
|
train_class_name?: string;
|
|
service_type?: string;
|
|
end_station_name?: string;
|
|
start_station_name?: string;
|
|
station_train_code?: string;
|
|
};
|
|
|
|
type RouteStationInfo = {
|
|
arrive_time: string;
|
|
station_name: string;
|
|
stopover_time: string;
|
|
station_no: number;
|
|
};
|
|
|
|
type InterlineData = {
|
|
all_lishi: string;
|
|
all_lishi_minutes: number;
|
|
arrive_date: string;
|
|
arrive_time: string;
|
|
end_station_code: string;
|
|
end_station_name: string;
|
|
first_train_no: string;
|
|
from_station_code: string;
|
|
from_station_name: string;
|
|
fullList: InterlineTicketData[];
|
|
isHeatTrain: string;
|
|
isOutStation: string;
|
|
lCWaitTime: string;
|
|
lishi_flag: string;
|
|
middle_date: string;
|
|
middle_station_code: string;
|
|
middle_station_name: string;
|
|
same_station: string;
|
|
same_train: string;
|
|
score: number;
|
|
score_str: string;
|
|
scretstr: string;
|
|
second_train_no: string;
|
|
start_time: string;
|
|
train_count: number;
|
|
train_date: string; // 出发时间
|
|
use_time: string;
|
|
wait_time: string;
|
|
wait_time_minutes: number;
|
|
};
|
|
|
|
type InterlineInfo = {
|
|
lishi: string;
|
|
start_time: string;
|
|
start_date: string;
|
|
middle_date: string;
|
|
arrive_date: string;
|
|
arrive_time: string;
|
|
from_station_code: string;
|
|
from_station_name: string;
|
|
middle_station_code: string;
|
|
middle_station_name: string;
|
|
end_station_code: string;
|
|
end_station_name: string;
|
|
start_train_code: string; // 用于过滤
|
|
first_train_no: string;
|
|
second_train_no: string;
|
|
train_count: number;
|
|
ticketList: TicketInfo[];
|
|
same_station: boolean;
|
|
same_train: boolean;
|
|
wait_time: string;
|
|
};
|
|
|
|
type InterlineTicketData = {
|
|
arrive_time: string;
|
|
bed_level_info: string;
|
|
controlled_train_flag: string;
|
|
country_flag: string;
|
|
day_difference: string;
|
|
dw_flag: string;
|
|
end_station_name: string;
|
|
end_station_telecode: string;
|
|
from_station_name: string;
|
|
from_station_no: string;
|
|
from_station_telecode: string;
|
|
gg_num: string;
|
|
gr_num: string;
|
|
is_support_card: string;
|
|
lishi: string;
|
|
local_arrive_time: string;
|
|
local_start_time: string;
|
|
qt_num: string;
|
|
rw_num: string;
|
|
rz_num: string;
|
|
seat_discount_info: string;
|
|
seat_types: string;
|
|
srrb_num: string;
|
|
start_station_name: string;
|
|
start_station_telecode: string;
|
|
start_time: string;
|
|
start_train_date: string;
|
|
station_train_code: string;
|
|
swz_num: string;
|
|
to_station_name: string;
|
|
to_station_no: string;
|
|
to_station_telecode: string;
|
|
train_no: string;
|
|
train_seat_feature: string;
|
|
trms_train_flag: string;
|
|
tz_num: string;
|
|
wz_num: string;
|
|
yb_num: string;
|
|
yp_info: string;
|
|
yw_num: string;
|
|
yz_num: string;
|
|
ze_num: string;
|
|
zy_num: string;
|
|
};
|
|
|
|
// #endregion
|
|
|
|
const ticket12306 = (function () {
|
|
|
|
const API_BASE = 'https://kyfw.12306.cn';
|
|
const WEB_URL = 'https://www.12306.cn/index/';
|
|
const LCQUERY_INIT_URL = 'https://kyfw.12306.cn/otn/lcQuery/init';
|
|
|
|
let LCQUERY_PATH: string | undefined = undefined;
|
|
const MISSING_STATIONS: StationData[] = [
|
|
{ station_id: '@cdd', station_name: '成 都东', station_code: 'WEI', station_pinyin: 'chengdudong', station_short: 'cdd', station_index: '', code: '1707', city: '成都', r1: '', r2: '' },
|
|
];
|
|
|
|
let STATIONS: Record<string, StationData> | undefined = undefined;
|
|
let CITY_STATIONS: Record<string, { station_code: string; station_name: string }[]> | undefined = undefined;
|
|
let CITY_CODES: Record<string, { station_code: string; station_name: string }> | undefined = undefined;
|
|
let NAME_STATIONS: Record<string, { station_code: string; station_name: string }> | undefined = undefined;
|
|
|
|
const SEAT_SHORT_TYPES = { swz: '商务座', tz: '特等座', zy: '一等座', ze: '二等座', gr: '高软卧', srrb: '动卧', rw: '软卧', yw: '硬卧', rz: '软座', yz: '硬座', wz: '无座', qt: '其他', gg: '', yb: '' };
|
|
const SEAT_TYPES = {
|
|
'9': { name: '商务座', short: 'swz' }, P: { name: '特等座', short: 'tz' }, M: { name: '一等座', short: 'zy' }, D: { name: '优选一等座', short: 'zy' }, O: { name: '二等座', short: 'ze' }, S: { name: '二等包座', short: 'ze' }, '6': { name: '高级软卧', short: 'gr' }, A: { name: '高级动卧', short: 'gr' }, '4': { name: '软卧', short: 'rw' }, I: { name: '一等卧', short: 'rw' }, F: { name: '动卧', short: 'rw' }, '3': { name: '硬卧', short: 'yw' }, J: { name: '二等卧', short: 'yw' }, '2': { name: '软座', short: 'rz' }, '1': { name: '硬座', short: 'yz' }, W: { name: '无座', short: 'wz' }, WZ: { name: '无座', short: 'wz' }, H: { name: '其他', short: 'qt' },
|
|
};
|
|
const DW_FLAGS = ['智能动车组', '复兴号', '静音车厢', '温馨动卧', '动感号', '支持选铺', '老年优惠'];
|
|
|
|
const client = OkHttp.newClient();
|
|
let initPromise: Promise<void> | undefined = undefined;
|
|
|
|
// #region 辅助函数
|
|
function formatDate(date: Date): string {
|
|
const year = date.getUTCFullYear();
|
|
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function parseDate(dateStr: string): Date { // yyyyMMdd
|
|
const year = parseInt(dateStr.substring(0, 4), 10);
|
|
const month = parseInt(dateStr.substring(4, 6), 10) - 1;
|
|
const day = parseInt(dateStr.substring(6, 8), 10);
|
|
return new Date(Date.UTC(year, month, day));
|
|
}
|
|
|
|
function getCurrentShanghaiDate(): Date {
|
|
const now = new Date();
|
|
return new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
|
}
|
|
|
|
function checkDate(dateStr: string): boolean { // yyyy-MM-dd
|
|
const todayInShanghai = getCurrentShanghaiDate();
|
|
todayInShanghai.setUTCHours(0, 0, 0, 0);
|
|
|
|
const parts = dateStr.split('-').map(p => parseInt(p, 10));
|
|
const inputDate = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
|
|
|
|
return inputDate.getTime() >= todayInShanghai.getTime();
|
|
}
|
|
|
|
function parseCookies(cookies: string[]): Record<string, string> {
|
|
const cookieRecord: Record<string, string> = {};
|
|
if (!cookies) return cookieRecord;
|
|
cookies.forEach((cookie) => {
|
|
const keyValuePart = cookie.split(';')[0];
|
|
const [key, value] = keyValuePart.split('=');
|
|
if (key && value) {
|
|
cookieRecord[key.trim()] = value.trim();
|
|
}
|
|
});
|
|
return cookieRecord;
|
|
}
|
|
|
|
function formatCookies(cookies: Record<string, string>): string {
|
|
return Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join('; ');
|
|
}
|
|
|
|
async function getCookie(): Promise<Record<string, string> | undefined> {
|
|
const url = `${API_BASE}/otn/leftTicket/init`;
|
|
try {
|
|
const response = await client.newRequest().url(url).build().execute();
|
|
const cookieHeader = response.headers && (response.headers['set-cookie'] || response.headers['Set-Cookie']);
|
|
if (cookieHeader) {
|
|
const parsed = parseCookies(Array.isArray(cookieHeader) ? cookieHeader : [cookieHeader]);
|
|
if (Object.keys(parsed).length > 0) {
|
|
return parsed;
|
|
}
|
|
}
|
|
// If no cookies are in the header, assume the client is stateful.
|
|
// Return an empty object to signal success and rely on the client's cookie jar.
|
|
return {};
|
|
} catch (error) {
|
|
console.error('Error getting 12306 cookie:', error);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
async function make12306Request<T>(url: string, params: Record<string, string> = {}, headers: Record<string, string> = {}): Promise<T | undefined> {
|
|
const queryString = Object.entries(params).map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
|
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
|
try {
|
|
const finalHeaders = { ...headers };
|
|
// If the cookie string is empty, remove it to let the client use its cookie jar.
|
|
if (finalHeaders['Cookie'] === '') {
|
|
delete finalHeaders['Cookie'];
|
|
}
|
|
const request = client.newRequest().url(fullUrl).method('GET').headers(finalHeaders);
|
|
const response = await request.build().execute();
|
|
if (!response.isSuccessful()) {
|
|
throw new Error(`HTTP error! status: ${response.statusCode}`);
|
|
}
|
|
return JSON.parse(response.content);
|
|
} catch (error) {
|
|
console.error(`Error making 12306 request to ${fullUrl}:`, error);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
async function make12306RequestHtml(url: string): Promise<string | undefined> {
|
|
try {
|
|
const request = client.newRequest().url(url).method('GET');
|
|
const response = await request.build().execute();
|
|
if (!response.isSuccessful()) {
|
|
throw new Error(`HTTP error! status: ${response.statusCode}`);
|
|
}
|
|
return response.content;
|
|
} catch (error) {
|
|
console.error(`Error fetching HTML from ${url}:`, error);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function parseTicketsData(rawData: string[]): TicketData[] {
|
|
const result: TicketData[] = [];
|
|
for (const item of rawData) {
|
|
const values = item.split('|');
|
|
const entry: Partial<TicketData> = {};
|
|
TicketDataKeys.forEach((key, index) => {
|
|
entry[key] = values[index];
|
|
});
|
|
result.push(entry as TicketData);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function extractPrices(yp_info: string, seat_discount_info: string, ticketData: TicketData | InterlineTicketData): Price[] {
|
|
const PRICE_STR_LENGTH = 10;
|
|
const DISCOUNT_STR_LENGTH = 5;
|
|
const prices: Price[] = [];
|
|
const discounts: { [key: string]: number } = {};
|
|
for (let i = 0; i < seat_discount_info.length / DISCOUNT_STR_LENGTH; i++) {
|
|
const discount_str = seat_discount_info.slice(i * DISCOUNT_STR_LENGTH, (i + 1) * DISCOUNT_STR_LENGTH);
|
|
discounts[discount_str[0]] = parseInt(discount_str.slice(1), 10);
|
|
}
|
|
|
|
for (let i = 0; i < yp_info.length / PRICE_STR_LENGTH; i++) {
|
|
const price_str = yp_info.slice(i * PRICE_STR_LENGTH, (i + 1) * PRICE_STR_LENGTH);
|
|
var seat_type_code;
|
|
if (parseInt(price_str.slice(6, 10), 10) >= 3000) {
|
|
seat_type_code = 'W'; // 为无座
|
|
} else if (!Object.keys(SEAT_TYPES).includes(price_str[0])) {
|
|
seat_type_code = 'H'; // 其他坐席
|
|
} else {
|
|
seat_type_code = price_str[0];
|
|
}
|
|
const seat_type = SEAT_TYPES[seat_type_code as keyof typeof SEAT_TYPES];
|
|
const price = parseInt(price_str.slice(1, 6), 10) / 10;
|
|
const discount = seat_type_code in discounts ? discounts[seat_type_code] : undefined;
|
|
prices.push({
|
|
seat_name: seat_type.name,
|
|
short: seat_type.short,
|
|
seat_type_code,
|
|
num: ticketData[`${seat_type.short}_num` as keyof (TicketData | InterlineTicketData)],
|
|
price,
|
|
discount,
|
|
});
|
|
}
|
|
return prices;
|
|
}
|
|
|
|
function extractDWFlags(dw_flag_str: string): string[] {
|
|
const dwFlagList = dw_flag_str.split('#');
|
|
let result: string[] = [];
|
|
if ('5' == dwFlagList[0]) { result.push(DW_FLAGS[0]); }
|
|
if (dwFlagList.length > 1 && '1' == dwFlagList[1]) { result.push(DW_FLAGS[1]); }
|
|
if (dwFlagList.length > 2) {
|
|
if ('Q' == dwFlagList[2].substring(0, 1)) { result.push(DW_FLAGS[2]); }
|
|
else if ('R' == dwFlagList[2].substring(0, 1)) { result.push(DW_FLAGS[3]); }
|
|
}
|
|
if (dwFlagList.length > 5 && 'D' == dwFlagList[5]) { result.push(DW_FLAGS[4]); }
|
|
if (dwFlagList.length > 6 && 'z' != dwFlagList[6]) { result.push(DW_FLAGS[5]); }
|
|
if (dwFlagList.length > 7 && 'z' != dwFlagList[7]) { result.push(DW_FLAGS[6]); }
|
|
return result;
|
|
}
|
|
|
|
function parseTicketsInfo(ticketsData: TicketData[], map: Record<string, string>): TicketInfo[] {
|
|
const result: TicketInfo[] = [];
|
|
for (const ticket of ticketsData) {
|
|
const prices = extractPrices(ticket.yp_info_new, ticket.seat_discount_info, ticket);
|
|
const dw_flag = extractDWFlags(ticket.dw_flag);
|
|
const startDate = parseDate(ticket.start_train_date);
|
|
const [startHours, startMinutes] = ticket.start_time.split(':').map(Number);
|
|
const [durationHours, durationMinutes] = ticket.lishi.split(':').map(Number);
|
|
|
|
const arriveDate = new Date(startDate);
|
|
arriveDate.setUTCHours(arriveDate.getUTCHours() + startHours + durationHours, arriveDate.getUTCMinutes() + startMinutes + durationMinutes);
|
|
|
|
result.push({
|
|
train_no: ticket.train_no,
|
|
start_date: formatDate(startDate),
|
|
arrive_date: formatDate(arriveDate),
|
|
start_train_code: ticket.station_train_code,
|
|
start_time: ticket.start_time,
|
|
arrive_time: ticket.arrive_time,
|
|
lishi: ticket.lishi,
|
|
from_station: map[ticket.from_station_telecode],
|
|
to_station: map[ticket.to_station_telecode],
|
|
from_station_telecode: ticket.from_station_telecode,
|
|
to_station_telecode: ticket.to_station_telecode,
|
|
prices: prices,
|
|
dw_flag: dw_flag,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function formatTicketStatus(num: string): string {
|
|
if (num.match(/^\d+$/)) {
|
|
const count = parseInt(num);
|
|
return count === 0 ? '无票' : `剩余${count}张票`;
|
|
}
|
|
switch (num) {
|
|
case '有': case '充足': return '有票';
|
|
case '无': case '--': case '': return '无票';
|
|
case '候补': return '无票需候补';
|
|
default: return `${num}票`;
|
|
}
|
|
}
|
|
|
|
function formatTicketsInfo(ticketsInfo: TicketInfo[]): string {
|
|
if (ticketsInfo.length === 0) return '没有查询到相关车次信息';
|
|
let result = '车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n';
|
|
ticketsInfo.forEach((ticketInfo) => {
|
|
let infoStr = `${ticketInfo.start_train_code}(实际车次train_no: ${ticketInfo.train_no}) ${ticketInfo.from_station}(telecode: ${ticketInfo.from_station_telecode}) -> ${ticketInfo.to_station}(telecode: ${ticketInfo.to_station_telecode}) ${ticketInfo.start_time} -> ${ticketInfo.arrive_time} 历时:${ticketInfo.lishi}`;
|
|
ticketInfo.prices.forEach((price) => {
|
|
infoStr += `\n- ${price.seat_name}: ${formatTicketStatus(price.num)} ${price.price}元`;
|
|
});
|
|
result += `${infoStr}\n`;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
const TRAIN_FILTERS = {
|
|
G: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('G') || t.start_train_code.startsWith('C'),
|
|
D: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('D'),
|
|
Z: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('Z'),
|
|
T: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('T'),
|
|
K: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('K'),
|
|
O: (t: TicketInfo | InterlineInfo) => !/^[GDZTK]/.test(t.start_train_code),
|
|
F: (t: TicketInfo | InterlineInfo) => 'dw_flag' in t ? t.dw_flag.includes('复兴号') : t.ticketList[0].dw_flag.includes('复兴号'),
|
|
S: (t: TicketInfo | InterlineInfo) => 'dw_flag' in t ? t.dw_flag.includes('智能动车组') : t.ticketList[0].dw_flag.includes('智能动车组'),
|
|
};
|
|
|
|
const TIME_COMPARETOR = {
|
|
startTime: (a: TicketInfo | InterlineInfo, b: TicketInfo | InterlineInfo) => new Date(`${a.start_date} ${a.start_time}`).getTime() - new Date(`${b.start_date} ${b.start_time}`).getTime(),
|
|
arriveTime: (a: TicketInfo | InterlineInfo, b: TicketInfo | InterlineInfo) => new Date(`${a.arrive_date} ${a.arrive_time}`).getTime() - new Date(`${b.arrive_date} ${b.arrive_time}`).getTime(),
|
|
duration: (a: TicketInfo | InterlineInfo, b: TicketInfo | InterlineInfo) => {
|
|
const [hA, mA] = a.lishi.split(':').map(Number);
|
|
const [hB, mB] = b.lishi.split(':').map(Number);
|
|
return (hA * 60 + mA) - (hB * 60 + mB);
|
|
},
|
|
};
|
|
|
|
function filterTicketsInfo<T extends TicketInfo | InterlineInfo>(ticketsInfo: T[], trainFilterFlags: string, sortFlag: string = '', sortReverse: boolean = false, limitedNum: number = 0): T[] {
|
|
let result = trainFilterFlags ? ticketsInfo.filter(t => [...trainFilterFlags].some(flag => TRAIN_FILTERS[flag as keyof typeof TRAIN_FILTERS](t))) : ticketsInfo;
|
|
if (Object.keys(TIME_COMPARETOR).includes(sortFlag)) {
|
|
result.sort(TIME_COMPARETOR[sortFlag as keyof typeof TIME_COMPARETOR]);
|
|
if (sortReverse) result.reverse();
|
|
}
|
|
return limitedNum > 0 ? result.slice(0, limitedNum) : result;
|
|
}
|
|
|
|
function parseRouteStationsInfo(routeStationsData: RouteStationData[]): RouteStationInfo[] {
|
|
return routeStationsData.map((routeStationData, index) => ({
|
|
arrive_time: index === 0 ? routeStationData.start_time : routeStationData.arrive_time,
|
|
station_name: routeStationData.station_name,
|
|
stopover_time: routeStationData.stopover_time,
|
|
station_no: parseInt(routeStationData.station_no),
|
|
}));
|
|
}
|
|
|
|
function parseInterlinesTicketInfo(interlineTicketsData: InterlineTicketData[]): TicketInfo[] {
|
|
return interlineTicketsData.map(ticket => {
|
|
const prices = extractPrices(ticket.yp_info, ticket.seat_discount_info, ticket);
|
|
const startDate = parseDate(ticket.start_train_date);
|
|
const [startHours, startMinutes] = ticket.start_time.split(':').map(Number);
|
|
const [durationHours, durationMinutes] = ticket.lishi.split(':').map(Number);
|
|
|
|
const arriveDate = new Date(startDate);
|
|
arriveDate.setUTCHours(arriveDate.getUTCHours() + startHours + durationHours, arriveDate.getUTCMinutes() + startMinutes + durationMinutes);
|
|
|
|
return {
|
|
train_no: ticket.train_no,
|
|
start_train_code: ticket.station_train_code,
|
|
start_date: formatDate(startDate),
|
|
arrive_date: formatDate(arriveDate),
|
|
start_time: ticket.start_time,
|
|
arrive_time: ticket.arrive_time,
|
|
lishi: ticket.lishi,
|
|
from_station: ticket.from_station_name,
|
|
to_station: ticket.to_station_name,
|
|
from_station_telecode: ticket.from_station_telecode,
|
|
to_station_telecode: ticket.to_station_telecode,
|
|
prices: prices,
|
|
dw_flag: extractDWFlags(ticket.dw_flag),
|
|
};
|
|
});
|
|
}
|
|
|
|
function extractLishi(all_lishi: string): string {
|
|
const match = all_lishi.match(/(?:(\d+)小时)?(\d+)分钟/);
|
|
if (!match) return '00:00';
|
|
const hours = (match[1] || '0').padStart(2, '0');
|
|
const minutes = (match[2] || '0').padStart(2, '0');
|
|
return `${hours}:${minutes}`;
|
|
}
|
|
|
|
function parseInterlinesInfo(interlineData: InterlineData[]): InterlineInfo[] {
|
|
return interlineData.map(ticket => ({
|
|
lishi: extractLishi(ticket.all_lishi),
|
|
start_time: ticket.start_time,
|
|
start_date: ticket.train_date,
|
|
middle_date: ticket.middle_date,
|
|
arrive_date: ticket.arrive_date,
|
|
arrive_time: ticket.arrive_time,
|
|
from_station_code: ticket.from_station_code,
|
|
from_station_name: ticket.from_station_name,
|
|
middle_station_code: ticket.middle_station_code,
|
|
middle_station_name: ticket.middle_station_name,
|
|
end_station_code: ticket.end_station_code,
|
|
end_station_name: ticket.end_station_name,
|
|
start_train_code: ticket.fullList[0].station_train_code,
|
|
first_train_no: ticket.first_train_no,
|
|
second_train_no: ticket.second_train_no,
|
|
train_count: ticket.train_count,
|
|
ticketList: parseInterlinesTicketInfo(ticket.fullList),
|
|
same_station: ticket.same_station == '0',
|
|
same_train: ticket.same_train == 'Y',
|
|
wait_time: ticket.wait_time,
|
|
}));
|
|
}
|
|
|
|
function formatInterlinesInfo(interlinesInfo: InterlineInfo[]): string {
|
|
if (interlinesInfo.length === 0) return '没有查询到相关的中转车次信息';
|
|
let result = '出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 | 换乘等待时间 | 总历时\n\n';
|
|
interlinesInfo.forEach((info) => {
|
|
result += `${info.start_date} ${info.start_time} -> ${info.arrive_date} ${info.arrive_time} | `;
|
|
result += `${info.from_station_name} -> ${info.middle_station_name} -> ${info.end_station_name} | `;
|
|
result += `${info.same_train ? '同车换乘' : info.same_station ? '同站换乘' : '换站换乘'} | ${info.wait_time} | ${info.lishi}\n\n`;
|
|
result += '\t' + formatTicketsInfo(info.ticketList).replace(/\n/g, '\n\t') + '\n';
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function parseStationsData(rawData: string): Record<string, StationData> {
|
|
const result: Record<string, StationData> = {};
|
|
const dataArray = rawData.split('|');
|
|
for (let i = 0; i < dataArray.length; i += 10) {
|
|
const group = dataArray.slice(i, i + 10);
|
|
if (group.length < 10) continue;
|
|
let station: Partial<StationData> = {};
|
|
StationDataKeys.forEach((key, index) => {
|
|
station[key] = group[index];
|
|
});
|
|
if (station.station_code) {
|
|
result[station.station_code!] = station as StationData;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function getStationsInternal(): Promise<Record<string, StationData>> {
|
|
const stationNameJSUrl = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js";
|
|
const stationNameJS = await make12306RequestHtml(stationNameJSUrl);
|
|
if (!stationNameJS) throw new Error('Error: get station name js file content failed.');
|
|
|
|
const rawDataMatch = stationNameJS.match(/var station_names\s*=\s*'(.*?)';/);
|
|
if (!rawDataMatch) throw new Error('Error: could not find station data in JS file.');
|
|
|
|
const rawData = rawDataMatch[1];
|
|
const stationsData = parseStationsData(rawData);
|
|
|
|
for (const station of MISSING_STATIONS) {
|
|
if (!stationsData[station.station_code]) {
|
|
stationsData[station.station_code] = station;
|
|
}
|
|
}
|
|
return stationsData;
|
|
}
|
|
|
|
async function getLCQueryPath(): Promise<string> {
|
|
const html = await make12306RequestHtml(LCQUERY_INIT_URL);
|
|
if (html == undefined) throw new Error('Error: get 12306 web page for LCQuery path failed.');
|
|
const match = html.match(/var lc_search_url = '(.+?)'/);
|
|
if (match == undefined) throw new Error('Error: get LCQuery path failed.');
|
|
return match[1];
|
|
}
|
|
|
|
async function init() {
|
|
if (initPromise) return initPromise;
|
|
initPromise = (async () => {
|
|
if (STATIONS) return;
|
|
try {
|
|
STATIONS = await getStationsInternal();
|
|
LCQUERY_PATH = await getLCQueryPath();
|
|
|
|
CITY_STATIONS = {};
|
|
for (const station of Object.values(STATIONS)) {
|
|
const city = station.city;
|
|
if (!CITY_STATIONS[city]) CITY_STATIONS[city] = [];
|
|
CITY_STATIONS[city].push({ station_code: station.station_code, station_name: station.station_name });
|
|
}
|
|
|
|
CITY_CODES = {};
|
|
for (const [city, stations] of Object.entries(CITY_STATIONS)) {
|
|
for (const station of stations) {
|
|
if (station.station_name == city) {
|
|
CITY_CODES[city] = station;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
NAME_STATIONS = {};
|
|
for (const station of Object.values(STATIONS)) {
|
|
NAME_STATIONS[station.station_name] = { station_code: station.station_code, station_name: station.station_name };
|
|
}
|
|
} catch (e) {
|
|
initPromise = undefined; // Reset promise on failure to allow retry
|
|
throw e;
|
|
}
|
|
})();
|
|
return initPromise;
|
|
}
|
|
// #endregion
|
|
|
|
// #region 工具函数实现
|
|
async function get_current_date(params: {}) {
|
|
const now = getCurrentShanghaiDate();
|
|
return formatDate(now);
|
|
}
|
|
|
|
async function get_stations_code_in_city(params: { city: string }) {
|
|
await init();
|
|
if (!(params.city in CITY_STATIONS!)) {
|
|
throw new Error('City not found.');
|
|
}
|
|
return CITY_STATIONS![params.city];
|
|
}
|
|
|
|
async function get_station_code_of_citys(params: { citys: string }) {
|
|
await init();
|
|
let result: Record<string, object> = {};
|
|
for (const city of params.citys.split('|')) {
|
|
if (!(city in CITY_CODES!)) {
|
|
result[city] = { error: '未检索到城市。' };
|
|
} else {
|
|
result[city] = CITY_CODES![city];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function get_station_code_by_names(params: { station_names: string }) {
|
|
await init();
|
|
let result: Record<string, object> = {};
|
|
for (let stationName of params.station_names.split('|')) {
|
|
stationName = stationName.endsWith('站') ? stationName.slice(0, -1) : stationName;
|
|
if (!(stationName in NAME_STATIONS!)) {
|
|
result[stationName] = { error: '未检索到车站。' };
|
|
} else {
|
|
result[stationName] = NAME_STATIONS![stationName];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function get_station_by_telecode(params: { station_telecode: string }) {
|
|
await init();
|
|
if (!STATIONS![params.station_telecode]) {
|
|
throw new Error('Station not found.');
|
|
}
|
|
return STATIONS![params.station_telecode];
|
|
}
|
|
|
|
async function get_tickets(params: { date: string, from_station: string, to_station: string, train_filter_flags?: string, sort_flag?: string, sort_reverse?: boolean, limited_num?: number }) {
|
|
await init();
|
|
if (!checkDate(params.date)) throw new Error('The date cannot be earlier than today.');
|
|
if (!STATIONS![params.from_station] || !STATIONS![params.to_station]) throw new Error('Station not found.');
|
|
|
|
const queryParams = {
|
|
'leftTicketDTO.train_date': params.date,
|
|
'leftTicketDTO.from_station': params.from_station,
|
|
'leftTicketDTO.to_station': params.to_station,
|
|
'purpose_codes': 'ADULT',
|
|
};
|
|
const queryUrl = `${API_BASE}/otn/leftTicket/query`;
|
|
const cookies = await getCookie();
|
|
if (!cookies) throw new Error('Get cookie failed. Check your network.');
|
|
|
|
const response = await make12306Request<any>(queryUrl, queryParams, { Cookie: formatCookies(cookies) });
|
|
if (!response || !response.data || !response.data.result) throw new Error('Get tickets data failed.');
|
|
|
|
const ticketsData = parseTicketsData(response.data.result);
|
|
const ticketsInfo = parseTicketsInfo(ticketsData, response.data.map);
|
|
const filteredTicketsInfo = filterTicketsInfo(ticketsInfo, params.train_filter_flags || '', params.sort_flag, params.sort_reverse, params.limited_num);
|
|
|
|
return formatTicketsInfo(filteredTicketsInfo);
|
|
}
|
|
|
|
async function get_interline_tickets(params: { date: string, from_station: string, to_station: string, middle_station?: string, show_wz?: boolean, train_filter_flags?: string, sort_flag?: string, sort_reverse?: boolean, limited_num?: number }) {
|
|
await init();
|
|
if (!checkDate(params.date)) throw new Error('The date cannot be earlier than today.');
|
|
if (!STATIONS![params.from_station] || !STATIONS![params.to_station]) throw new Error('Station not found.');
|
|
|
|
const cookies = await getCookie();
|
|
if (!cookies) throw new Error('Get cookie failed. Check your network.');
|
|
|
|
const limited_num = params.limited_num || 10;
|
|
let interlineData: InterlineData[] = [];
|
|
const queryParams = {
|
|
'train_date': params.date,
|
|
'from_station_telecode': params.from_station,
|
|
'to_station_telecode': params.to_station,
|
|
'middle_station': params.middle_station || '',
|
|
'result_index': '0',
|
|
'can_query': 'Y',
|
|
'isShowWZ': params.show_wz ? 'Y' : 'N',
|
|
'purpose_codes': '00',
|
|
'channel': 'E',
|
|
};
|
|
|
|
while (interlineData.length < limited_num) {
|
|
const response = await make12306Request<any>(`${API_BASE}${LCQUERY_PATH}`, queryParams, { Cookie: formatCookies(cookies) });
|
|
if (!response) throw new Error('Request interline tickets data failed.');
|
|
if (typeof response.data === 'string') return `很抱歉,未查到相关的列车余票。(${response.errorMsg})`;
|
|
|
|
interlineData.push(...response.data.middleList);
|
|
if (response.data.can_query === 'N' || !response.data.middleList || response.data.middleList.length === 0) break;
|
|
queryParams.result_index = response.data.result_index.toString();
|
|
}
|
|
|
|
const interlineTicketsInfo = parseInterlinesInfo(interlineData);
|
|
const filtered = filterTicketsInfo(interlineTicketsInfo, params.train_filter_flags || '', params.sort_flag, params.sort_reverse, limited_num);
|
|
|
|
return formatInterlinesInfo(filtered);
|
|
}
|
|
|
|
async function get_train_route_stations(params: { train_no: string, from_station_telecode: string, to_station_telecode: string, depart_date: string }) {
|
|
await init();
|
|
const queryParams = {
|
|
'train_no': params.train_no,
|
|
'from_station_telecode': params.from_station_telecode,
|
|
'to_station_telecode': params.to_station_telecode,
|
|
'depart_date': params.depart_date,
|
|
};
|
|
const queryUrl = `${API_BASE}/otn/czxx/queryByTrainNo`;
|
|
const cookies = await getCookie();
|
|
if (!cookies) throw new Error('Get cookie failed.');
|
|
|
|
const response = await make12306Request<any>(queryUrl, queryParams, { Cookie: formatCookies(cookies) });
|
|
if (!response || !response.data || !response.data.data) throw new Error('Get train route stations failed.');
|
|
|
|
const routeStationsInfo = parseRouteStationsInfo(response.data.data);
|
|
if (routeStationsInfo.length === 0) return '未查询到相关车次信息。';
|
|
|
|
return routeStationsInfo;
|
|
}
|
|
// #endregion
|
|
|
|
async function wrap<T>(func: (params: any) => Promise<any>, params: any, successMessage: string, failMessage: string) {
|
|
try {
|
|
const result = await func(params);
|
|
complete({ success: true, message: successMessage, data: result });
|
|
} catch (error: any) {
|
|
console.error(`Function ${func.name} failed! Error: ${error.message}`);
|
|
complete({ success: false, message: `${failMessage}: ${error.message}`, error_stack: error.stack });
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log("--- 开始测试 12306 工具包 ---");
|
|
|
|
try {
|
|
await init();
|
|
|
|
console.log("\n[1/8] 测试 get_current_date...");
|
|
const dateResult = await get_current_date({});
|
|
console.log("测试结果:", JSON.stringify(dateResult, undefined, 2));
|
|
const testDate = dateResult as string;
|
|
|
|
console.log("\n[2/8] 测试 get_stations_code_in_city (北京)...");
|
|
const cityStations = await get_stations_code_in_city({ city: '北京' });
|
|
console.log("测试结果:", JSON.stringify(cityStations, undefined, 2));
|
|
|
|
console.log("\n[3/8] 测试 get_station_code_of_citys (北京|上海)...");
|
|
const cityCodesResult = await get_station_code_of_citys({ citys: '北京|上海' });
|
|
console.log("测试结果:", JSON.stringify(cityCodesResult, undefined, 2));
|
|
const beijingCode = (cityCodesResult as any)['北京'].station_code;
|
|
const shanghaiCode = (cityCodesResult as any)['上海'].station_code;
|
|
|
|
console.log("\n[4/8] 测试 get_station_code_by_names (北京南|上海虹桥)...");
|
|
const stationCodesResult = await get_station_code_by_names({ station_names: '北京南|上海虹桥' });
|
|
console.log("测试结果:", JSON.stringify(stationCodesResult, undefined, 2));
|
|
const beijingnanCode = (stationCodesResult as any)['北京南'].station_code;
|
|
const shanghaihongqiaoCode = (stationCodesResult as any)['上海虹桥'].station_code;
|
|
|
|
console.log("\n[5/8] 测试 get_station_by_telecode (VNP)...");
|
|
const stationInfo = await get_station_by_telecode({ station_telecode: 'VNP' }); // VNP is 北京
|
|
console.log("测试结果:", JSON.stringify(stationInfo, undefined, 2));
|
|
|
|
console.log(`\n[6/8] 测试 get_tickets (${testDate}, from: 北京南, to: 上海虹桥)...`);
|
|
const tickets = await get_tickets({
|
|
date: testDate,
|
|
from_station: beijingnanCode,
|
|
to_station: shanghaihongqiaoCode,
|
|
train_filter_flags: 'G'
|
|
});
|
|
console.log("测试结果 (部分):", (tickets as string).substring(0, 400) + "...");
|
|
|
|
console.log(`\n[7/8] 测试 get_interline_tickets (${testDate}, from: 北京, to: 上海)...`);
|
|
const interlineTickets = await get_interline_tickets({
|
|
date: testDate,
|
|
from_station: beijingCode,
|
|
to_station: shanghaiCode,
|
|
limited_num: 2
|
|
});
|
|
console.log("测试结果 (部分):", (interlineTickets as string).substring(0, 400) + "...");
|
|
|
|
console.log(`\n[8/8] 测试 get_train_route_stations...`);
|
|
const ticketsResultForRoute = await get_tickets({ date: testDate, from_station: beijingnanCode, to_station: shanghaihongqiaoCode });
|
|
const trainNoMatch = (ticketsResultForRoute as string).match(/train_no: (\w+)/);
|
|
if (trainNoMatch && trainNoMatch[1]) {
|
|
const trainNo = trainNoMatch[1];
|
|
console.log(`使用车次 ${trainNo} 进行测试...`);
|
|
const routeStations = await get_train_route_stations({
|
|
train_no: trainNo,
|
|
from_station_telecode: beijingnanCode,
|
|
to_station_telecode: shanghaihongqiaoCode,
|
|
depart_date: testDate
|
|
});
|
|
console.log("测试结果:", JSON.stringify(routeStations, undefined, 2));
|
|
} else {
|
|
console.log("未从 get_tickets 结果中找到可用车次来测试 get_train_route_stations。");
|
|
}
|
|
|
|
} catch (e: any) {
|
|
console.error("测试主函数出现错误:", e.message, e.stack);
|
|
complete({ success: false, message: `测试失败: ${e.message}` });
|
|
return;
|
|
}
|
|
|
|
console.log("\n--- 12306 工具包测试完成 ---");
|
|
complete({ success: true, message: "所有测试已成功或已记录错误。" });
|
|
}
|
|
|
|
return {
|
|
get_current_date: (p: any) => wrap(get_current_date, p, '获取当前日期成功', '获取当前日期失败'),
|
|
get_stations_code_in_city: (p: any) => wrap(get_stations_code_in_city, p, '查询成功', '查询失败'),
|
|
get_station_code_of_citys: (p: any) => wrap(get_station_code_of_citys, p, '查询成功', '查询失败'),
|
|
get_station_code_by_names: (p: any) => wrap(get_station_code_by_names, p, '查询成功', '查询失败'),
|
|
get_station_by_telecode: (p: any) => wrap(get_station_by_telecode, p, '查询成功', '查询失败'),
|
|
get_tickets: (p: any) => wrap(get_tickets, p, '查询余票成功', '查询余票失败'),
|
|
get_interline_tickets: (p: any) => wrap(get_interline_tickets, p, '查询中转票成功', '查询中转票失败'),
|
|
get_train_route_stations: (p: any) => wrap(get_train_route_stations, p, '查询经停站成功', '查询经停站失败'),
|
|
main: main,
|
|
};
|
|
})();
|
|
|
|
exports.get_current_date = ticket12306.get_current_date;
|
|
exports.get_stations_code_in_city = ticket12306.get_stations_code_in_city;
|
|
exports.get_station_code_of_citys = ticket12306.get_station_code_of_citys;
|
|
exports.get_station_code_by_names = ticket12306.get_station_code_by_names;
|
|
exports.get_station_by_telecode = ticket12306.get_station_by_telecode;
|
|
exports.get_tickets = ticket12306.get_tickets;
|
|
exports.get_interline_tickets = ticket12306.get_interline_tickets;
|
|
exports.get_train_route_stations = ticket12306.get_train_route_stations;
|
|
exports.main = ticket12306.main;
|