- 新增 operit_runtime.py: 跨平台 Python 运行时 - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker - 实现 OkHttp 链式 HTTP 客户端兼容层 - 实现 complete()/exports/console 全局对象 - 实现 OperitScriptLoader 脚本加载器 - 支持 Windows/macOS/Linux 三平台 - 新增 operit_py/ 目录: 47 个 Python 翻译脚本 - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader - 网络类: extended_http_tools, network_test - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant - 系统类: system_tools, super_admin, daily_life, operit_editor - 对话类: ai_chat, extended_chat, qq_intelligent - 工作流类: workflow, vflow_trigger, tasker - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser - 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册 - Android 专属功能已降级处理(保留接口,返回不支持提示)
745 lines
35 KiB
Python
745 lines
35 KiB
Python
# 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'."}, "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。", "en": "Get the representative station_code for a Chinese city name."}, "parameters": [{"name": "citys", "description": {"zh": "要查询的城市,比如'北京'。若要查询多个城市,请用|分割。", "en": "City to query, e.g. '北京'. For multiple cities, separate with |."}, "type": "string", "required": True}]},
|
|
# {"name": "get_station_code_by_names", "description": {"zh": "通过具体的中文车站名查询其 station_code 和车站名。", "en": "Given a specific Chinese station name, return its station_code and station name."}, "parameters": [{"name": "station_names", "description": {"zh": "具体的中文车站名称,例如:'北京南', '上海虹桥'。多个站点用|分割。", "en": "Specific Chinese station names, e.g. '北京南', '上海虹桥'. For multiple stations, separate with |."}, "type": "string", "required": True}]},
|
|
# {"name": "get_station_by_telecode", "description": {"zh": "通过车站的 station_telecode 查询车站的详细信息。", "en": "Query station details by station_telecode."}, "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'。", "en": "Query date in 'yyyy-MM-dd'."}, "type": "string", "required": True},
|
|
# {"name": "from_station", "description": {"zh": "出发地的 station_code。", "en": "Origin station_code."}, "type": "string", "required": True},
|
|
# {"name": "to_station", "description": {"zh": "到达地的 station_code。", "en": "Destination station_code."}, "type": "string", "required": True},
|
|
# {"name": "train_filter_flags", "description": {"zh": "车次筛选条件,默认为空。可选标志:[G,D,Z,T,K,O,F,S]", "en": "Train filter flags. Options: [G,D,Z,T,K,O,F,S]."}, "type": "string", "required": False},
|
|
# {"name": "sort_flag", "description": {"zh": "排序方式。可选标志:[startTime, arriveTime, duration]", "en": "Sort mode. Options: startTime / arriveTime / duration."}, "type": "string", "required": False},
|
|
# {"name": "sort_reverse", "description": {"zh": "是否逆向排序结果,默认为false。", "en": "Reverse sort order (default: false)."}, "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'。", "en": "Query date in 'yyyy-MM-dd'."}, "type": "string", "required": True},
|
|
# {"name": "from_station", "description": {"zh": "出发地的 station_code。", "en": "Origin station_code."}, "type": "string", "required": True},
|
|
# {"name": "to_station", "description": {"zh": "到达地的 station_code。", "en": "Destination station_code."}, "type": "string", "required": True},
|
|
# {"name": "middle_station", "description": {"zh": "中转地的 station_code,可选。", "en": "Optional transfer station station_code."}, "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": "车次筛选条件,默认为空。", "en": "Train filter flags. Default empty."}, "type": "string", "required": False},
|
|
# {"name": "sort_flag", "description": {"zh": "排序方式。", "en": "Sort mode."}, "type": "string", "required": False},
|
|
# {"name": "sort_reverse", "description": {"zh": "是否逆向排序结果,默认为false。", "en": "Reverse sort order (default: false)."}, "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."}, "parameters": [
|
|
# {"name": "train_no", "description": {"zh": "要查询的实际车次编号 train_no,例如 '240000G10336'。", "en": "Actual train number train_no, e.g. '240000G10336'."}, "type": "string", "required": True},
|
|
# {"name": "from_station_telecode", "description": {"zh": "出发站的 station_telecode (3位字母编码)。", "en": "station_telecode (3-letter code) of the origin station."}, "type": "string", "required": True},
|
|
# {"name": "to_station_telecode", "description": {"zh": "到达站的 station_telecode (3位字母编码)。", "en": "station_telecode (3-letter code) of the destination station."}, "type": "string", "required": True},
|
|
# {"name": "depart_date", "description": {"zh": "列车从出发站出发的日期 (格式: yyyy-MM-dd)。", "en": "Departure date from the origin station (format: yyyy-MM-dd)."}, "type": "string", "required": True}
|
|
# ]}
|
|
# ]
|
|
# }
|
|
|
|
import json
|
|
import re
|
|
import time
|
|
import math
|
|
from datetime import datetime, timedelta, timezone
|
|
from urllib.parse import quote, unquote
|
|
|
|
Error = Exception
|
|
|
|
API_BASE = 'https://kyfw.12306.cn'
|
|
WEB_URL = 'https://www.12306.cn/index/'
|
|
LCQUERY_INIT_URL = 'https://kyfw.12306.cn/otn/lcQuery/init'
|
|
|
|
LCQUERY_PATH = None
|
|
MISSING_STATIONS = [
|
|
{"station_id": "@cdd", "station_name": "成 都东", "station_code": "WEI", "station_pinyin": "chengdudong", "station_short": "cdd", "station_index": "", "code": "1707", "city": "成都", "r1": "", "r2": ""},
|
|
]
|
|
|
|
STATIONS = None
|
|
CITY_STATIONS = None
|
|
CITY_CODES = None
|
|
NAME_STATIONS = None
|
|
|
|
SEAT_SHORT_TYPES = {"swz": "商务座", "tz": "特等座", "zy": "一等座", "ze": "二等座", "gr": "高软卧", "srrb": "动卧", "rw": "软卧", "yw": "硬卧", "rz": "软座", "yz": "硬座", "wz": "无座", "qt": "其他", "gg": "", "yb": ""}
|
|
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"},
|
|
}
|
|
DW_FLAGS = ['智能动车组', '复兴号', '静音车厢', '温馨动卧', '动感号', '支持选铺', '老年优惠']
|
|
|
|
TicketDataKeys = [
|
|
'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',
|
|
]
|
|
|
|
StationDataKeys = [
|
|
'station_id', 'station_name', 'station_code', 'station_pinyin', 'station_short',
|
|
'station_index', 'code', 'city', 'r1', 'r2',
|
|
]
|
|
|
|
client = OkHttp.newClient()
|
|
initPromise = None
|
|
|
|
|
|
def formatDate(date):
|
|
year = date.year
|
|
month = str(date.month).zfill(2)
|
|
day = str(date.day).zfill(2)
|
|
return f"{year}-{month}-{day}"
|
|
|
|
|
|
def parseDate(dateStr):
|
|
year = int(dateStr[0:4])
|
|
month = int(dateStr[4:6])
|
|
day = int(dateStr[6:8])
|
|
return datetime(year, month, day, tzinfo=timezone.utc)
|
|
|
|
|
|
def getCurrentShanghaiDate():
|
|
now = datetime.now(timezone.utc)
|
|
return now + timedelta(hours=8)
|
|
|
|
|
|
def checkDate(dateStr):
|
|
todayInShanghai = getCurrentShanghaiDate()
|
|
todayInShanghai = todayInShanghai.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
parts = [int(p) for p in dateStr.split('-')]
|
|
inputDate = datetime(parts[0], parts[1], parts[2], tzinfo=timezone.utc)
|
|
return inputDate >= todayInShanghai
|
|
|
|
|
|
def parseCookies(cookies):
|
|
cookieRecord = {}
|
|
if not cookies:
|
|
return cookieRecord
|
|
for cookie in cookies:
|
|
keyValuePart = cookie.split(';')[0]
|
|
if '=' in keyValuePart:
|
|
key, value = keyValuePart.split('=', 1)
|
|
if key and value:
|
|
cookieRecord[key.strip()] = value.strip()
|
|
return cookieRecord
|
|
|
|
|
|
def formatCookies(cookies):
|
|
return '; '.join([f"{k}={v}" for k, v in cookies.items()])
|
|
|
|
|
|
async def getCookie():
|
|
url = f"{API_BASE}/otn/leftTicket/init"
|
|
try:
|
|
response = await client.newRequest().url(url).build().execute()
|
|
cookieHeader = None
|
|
if response.headers:
|
|
cookieHeader = response.headers.get('set-cookie') or response.headers.get('Set-Cookie')
|
|
if cookieHeader:
|
|
parsed = parseCookies(cookieHeader if isinstance(cookieHeader, list) else [cookieHeader])
|
|
if len(parsed) > 0:
|
|
return parsed
|
|
return {}
|
|
except Exception as error:
|
|
console.error('Error getting 12306 cookie:', error)
|
|
return None
|
|
|
|
|
|
async def make12306Request(url, params=None, headers=None):
|
|
if params is None:
|
|
params = {}
|
|
if headers is None:
|
|
headers = {}
|
|
queryString = '&'.join([f"{quote(k)}={quote(str(v))}" for k, v in params.items()])
|
|
fullUrl = f"{url}?{queryString}" if queryString else url
|
|
try:
|
|
finalHeaders = dict(headers)
|
|
if finalHeaders.get('Cookie') == '':
|
|
del finalHeaders['Cookie']
|
|
request = client.newRequest().url(fullUrl).method("GET").headers(finalHeaders)
|
|
response = await request.build().execute()
|
|
if not response.isSuccessful():
|
|
raise Error(f"HTTP error! status: {response.statusCode}")
|
|
return json.loads(response.content)
|
|
except Exception as error:
|
|
console.error(f"Error making 12306 request to {fullUrl}:", error)
|
|
return None
|
|
|
|
|
|
async def make12306RequestHtml(url):
|
|
try:
|
|
request = client.newRequest().url(url).method("GET")
|
|
response = await request.build().execute()
|
|
if not response.isSuccessful():
|
|
raise Error(f"HTTP error! status: {response.statusCode}")
|
|
return response.content
|
|
except Exception as error:
|
|
console.error(f"Error fetching HTML from {url}:", error)
|
|
return None
|
|
|
|
|
|
def parseTicketsData(rawData):
|
|
result = []
|
|
for item in rawData:
|
|
values = item.split('|')
|
|
entry = {}
|
|
for index, key in enumerate(TicketDataKeys):
|
|
entry[key] = values[index] if index < len(values) else ""
|
|
result.append(entry)
|
|
return result
|
|
|
|
|
|
def extractPrices(yp_info, seat_discount_info, ticketData):
|
|
PRICE_STR_LENGTH = 10
|
|
DISCOUNT_STR_LENGTH = 5
|
|
prices = []
|
|
discounts = {}
|
|
for i in range(int(len(seat_discount_info) / DISCOUNT_STR_LENGTH)):
|
|
discount_str = seat_discount_info[i * DISCOUNT_STR_LENGTH:(i + 1) * DISCOUNT_STR_LENGTH]
|
|
if len(discount_str) > 0:
|
|
discounts[discount_str[0]] = int(discount_str[1:])
|
|
|
|
for i in range(int(len(yp_info) / PRICE_STR_LENGTH)):
|
|
price_str = yp_info[i * PRICE_STR_LENGTH:(i + 1) * PRICE_STR_LENGTH]
|
|
if len(price_str) < 10:
|
|
continue
|
|
if int(price_str[6:10]) >= 3000:
|
|
seat_type_code = 'W'
|
|
elif price_str[0] not in SEAT_TYPES:
|
|
seat_type_code = 'H'
|
|
else:
|
|
seat_type_code = price_str[0]
|
|
seat_type = SEAT_TYPES.get(seat_type_code, {"name": "其他", "short": "qt"})
|
|
price = int(price_str[1:6]) / 10
|
|
discount = discounts.get(seat_type_code)
|
|
prices.append({
|
|
"seat_name": seat_type["name"],
|
|
"short": seat_type["short"],
|
|
"seat_type_code": seat_type_code,
|
|
"num": ticketData.get(f"{seat_type['short']}_num", ""),
|
|
"price": price,
|
|
"discount": discount,
|
|
})
|
|
return prices
|
|
|
|
|
|
def extractDWFlags(dw_flag_str):
|
|
dwFlagList = dw_flag_str.split('#')
|
|
result = []
|
|
if len(dwFlagList) > 0 and '5' == dwFlagList[0]:
|
|
result.append(DW_FLAGS[0])
|
|
if len(dwFlagList) > 1 and '1' == dwFlagList[1]:
|
|
result.append(DW_FLAGS[1])
|
|
if len(dwFlagList) > 2:
|
|
if dwFlagList[2][:1] == 'Q':
|
|
result.append(DW_FLAGS[2])
|
|
elif dwFlagList[2][:1] == 'R':
|
|
result.append(DW_FLAGS[3])
|
|
if len(dwFlagList) > 5 and 'D' == dwFlagList[5]:
|
|
result.append(DW_FLAGS[4])
|
|
if len(dwFlagList) > 6 and 'z' != dwFlagList[6]:
|
|
result.append(DW_FLAGS[5])
|
|
if len(dwFlagList) > 7 and 'z' != dwFlagList[7]:
|
|
result.append(DW_FLAGS[6])
|
|
return result
|
|
|
|
|
|
def parseTicketsInfo(ticketsData, map_):
|
|
result = []
|
|
for ticket in ticketsData:
|
|
prices = extractPrices(ticket.get("yp_info_new", ""), ticket.get("seat_discount_info", ""), ticket)
|
|
dw_flag = extractDWFlags(ticket.get("dw_flag", ""))
|
|
startDate = parseDate(ticket.get("start_train_date", "20000101"))
|
|
startParts = [int(x) for x in ticket.get("start_time", "00:00").split(':')]
|
|
durationParts = [int(x) for x in ticket.get("lishi", "00:00").split(':')]
|
|
startHours, startMinutes = startParts[0], startParts[1]
|
|
durationHours, durationMinutes = durationParts[0], durationParts[1]
|
|
arriveDate = startDate + timedelta(hours=startHours + durationHours, minutes=startMinutes + durationMinutes)
|
|
result.append({
|
|
"train_no": ticket.get("train_no", ""),
|
|
"start_date": formatDate(startDate),
|
|
"arrive_date": formatDate(arriveDate),
|
|
"start_train_code": ticket.get("station_train_code", ""),
|
|
"start_time": ticket.get("start_time", ""),
|
|
"arrive_time": ticket.get("arrive_time", ""),
|
|
"lishi": ticket.get("lishi", ""),
|
|
"from_station": map_.get(ticket.get("from_station_telecode", ""), ""),
|
|
"to_station": map_.get(ticket.get("to_station_telecode", ""), ""),
|
|
"from_station_telecode": ticket.get("from_station_telecode", ""),
|
|
"to_station_telecode": ticket.get("to_station_telecode", ""),
|
|
"prices": prices,
|
|
"dw_flag": dw_flag,
|
|
})
|
|
return result
|
|
|
|
|
|
def formatTicketStatus(num):
|
|
if num and re.match(r'^\d+$', str(num)):
|
|
count = int(num)
|
|
return '无票' if count == 0 else f'剩余{count}张票'
|
|
if num in ('有', '充足'):
|
|
return '有票'
|
|
if num in ('无', '--', ''):
|
|
return '无票'
|
|
if num == '候补':
|
|
return '无票需候补'
|
|
return f'{num}票'
|
|
|
|
|
|
def formatTicketsInfo(ticketsInfo):
|
|
if len(ticketsInfo) == 0:
|
|
return '没有查询到相关车次信息'
|
|
result = '车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n'
|
|
for ticketInfo in ticketsInfo:
|
|
infoStr = f"{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']}"
|
|
for price in ticketInfo["prices"]:
|
|
infoStr += f"\n- {price['seat_name']}: {formatTicketStatus(price['num'])} {price['price']}元"
|
|
result += f"{infoStr}\n"
|
|
return result
|
|
|
|
|
|
def trainFilterG(t):
|
|
return t["start_train_code"].startswith('G') or t["start_train_code"].startswith('C')
|
|
|
|
|
|
def trainFilterD(t):
|
|
return t["start_train_code"].startswith('D')
|
|
|
|
|
|
def trainFilterZ(t):
|
|
return t["start_train_code"].startswith('Z')
|
|
|
|
|
|
def trainFilterT(t):
|
|
return t["start_train_code"].startswith('T')
|
|
|
|
|
|
def trainFilterK(t):
|
|
return t["start_train_code"].startswith('K')
|
|
|
|
|
|
def trainFilterO(t):
|
|
return not re.match(r'^[GDZTK]', t["start_train_code"])
|
|
|
|
|
|
def trainFilterF(t):
|
|
if "dw_flag" in t:
|
|
return '复兴号' in t["dw_flag"]
|
|
return '复兴号' in t["ticketList"][0]["dw_flag"]
|
|
|
|
|
|
def trainFilterS(t):
|
|
if "dw_flag" in t:
|
|
return '智能动车组' in t["dw_flag"]
|
|
return '智能动车组' in t["ticketList"][0]["dw_flag"]
|
|
|
|
|
|
TRAIN_FILTERS = {'G': trainFilterG, 'D': trainFilterD, 'Z': trainFilterZ, 'T': trainFilterT, 'K': trainFilterK, 'O': trainFilterO, 'F': trainFilterF, 'S': trainFilterS}
|
|
|
|
|
|
def timeCompareStartTime(a, b):
|
|
da = datetime.strptime(f"{a['start_date']} {a['start_time']}", "%Y-%m-%d %H:%M")
|
|
db = datetime.strptime(f"{b['start_date']} {b['start_time']}", "%Y-%m-%d %H:%M")
|
|
return (da - db).total_seconds() * 1000
|
|
|
|
|
|
def timeCompareArriveTime(a, b):
|
|
da = datetime.strptime(f"{a['arrive_date']} {a['arrive_time']}", "%Y-%m-%d %H:%M")
|
|
db = datetime.strptime(f"{b['arrive_date']} {b['arrive_time']}", "%Y-%m-%d %H:%M")
|
|
return (da - db).total_seconds() * 1000
|
|
|
|
|
|
def timeCompareDuration(a, b):
|
|
hA, mA = [int(x) for x in a["lishi"].split(':')]
|
|
hB, mB = [int(x) for x in b["lishi"].split(':')]
|
|
return (hA * 60 + mA) - (hB * 60 + mB)
|
|
|
|
|
|
TIME_COMPARETOR = {"startTime": timeCompareStartTime, "arriveTime": timeCompareArriveTime, "duration": timeCompareDuration}
|
|
|
|
|
|
def filterTicketsInfo(ticketsInfo, trainFilterFlags, sortFlag='', sortReverse=False, limitedNum=0):
|
|
if trainFilterFlags:
|
|
flags = list(trainFilterFlags)
|
|
result = [t for t in ticketsInfo if any(TRAIN_FILTERS.get(f, lambda x: False)(t) for f in flags)]
|
|
else:
|
|
result = list(ticketsInfo)
|
|
if sortFlag in TIME_COMPARETOR:
|
|
result.sort(key=lambda x: x)
|
|
# Python sort with custom comparator
|
|
import functools
|
|
result = sorted(result, key=functools.cmp_to_key(TIME_COMPARETOR[sortFlag]))
|
|
if sortReverse:
|
|
result.reverse()
|
|
return result[:limitedNum] if limitedNum > 0 else result
|
|
|
|
|
|
def parseRouteStationsInfo(routeStationsData):
|
|
result = []
|
|
for index, routeStationData in enumerate(routeStationsData):
|
|
result.append({
|
|
"arrive_time": routeStationData.get("start_time") if index == 0 else routeStationData.get("arrive_time", ""),
|
|
"station_name": routeStationData.get("station_name", ""),
|
|
"stopover_time": routeStationData.get("stopover_time", ""),
|
|
"station_no": int(routeStationData.get("station_no", 0)),
|
|
})
|
|
return result
|
|
|
|
|
|
def parseInterlinesTicketInfo(interlineTicketsData):
|
|
result = []
|
|
for ticket in interlineTicketsData:
|
|
prices = extractPrices(ticket.get("yp_info", ""), ticket.get("seat_discount_info", ""), ticket)
|
|
startDate = parseDate(ticket.get("start_train_date", "20000101"))
|
|
startParts = [int(x) for x in ticket.get("start_time", "00:00").split(':')]
|
|
durationParts = [int(x) for x in ticket.get("lishi", "00:00").split(':')]
|
|
startHours, startMinutes = startParts[0], startParts[1]
|
|
durationHours, durationMinutes = durationParts[0], durationParts[1]
|
|
arriveDate = startDate + timedelta(hours=startHours + durationHours, minutes=startMinutes + durationMinutes)
|
|
result.append({
|
|
"train_no": ticket.get("train_no", ""),
|
|
"start_train_code": ticket.get("station_train_code", ""),
|
|
"start_date": formatDate(startDate),
|
|
"arrive_date": formatDate(arriveDate),
|
|
"start_time": ticket.get("start_time", ""),
|
|
"arrive_time": ticket.get("arrive_time", ""),
|
|
"lishi": ticket.get("lishi", ""),
|
|
"from_station": ticket.get("from_station_name", ""),
|
|
"to_station": ticket.get("to_station_name", ""),
|
|
"from_station_telecode": ticket.get("from_station_telecode", ""),
|
|
"to_station_telecode": ticket.get("to_station_telecode", ""),
|
|
"prices": prices,
|
|
"dw_flag": extractDWFlags(ticket.get("dw_flag", "")),
|
|
})
|
|
return result
|
|
|
|
|
|
def extractLishi(all_lishi):
|
|
match = re.search(r'(?:(\d+)小时)?(\d+)分钟', all_lishi)
|
|
if not match:
|
|
return '00:00'
|
|
hours = (match.group(1) or '0').zfill(2)
|
|
minutes = (match.group(2) or '0').zfill(2)
|
|
return f"{hours}:{minutes}"
|
|
|
|
|
|
def parseInterlinesInfo(interlineData):
|
|
result = []
|
|
for ticket in interlineData:
|
|
fullList = ticket.get("fullList") or []
|
|
result.append({
|
|
"lishi": extractLishi(ticket.get("all_lishi", "")),
|
|
"start_time": ticket.get("start_time", ""),
|
|
"start_date": ticket.get("train_date", ""),
|
|
"middle_date": ticket.get("middle_date", ""),
|
|
"arrive_date": ticket.get("arrive_date", ""),
|
|
"arrive_time": ticket.get("arrive_time", ""),
|
|
"from_station_code": ticket.get("from_station_code", ""),
|
|
"from_station_name": ticket.get("from_station_name", ""),
|
|
"middle_station_code": ticket.get("middle_station_code", ""),
|
|
"middle_station_name": ticket.get("middle_station_name", ""),
|
|
"end_station_code": ticket.get("end_station_code", ""),
|
|
"end_station_name": ticket.get("end_station_name", ""),
|
|
"start_train_code": fullList[0].get("station_train_code", "") if fullList else "",
|
|
"first_train_no": ticket.get("first_train_no", ""),
|
|
"second_train_no": ticket.get("second_train_no", ""),
|
|
"train_count": ticket.get("train_count", 0),
|
|
"ticketList": parseInterlinesTicketInfo(fullList),
|
|
"same_station": ticket.get("same_station") == '0',
|
|
"same_train": ticket.get("same_train") == 'Y',
|
|
"wait_time": ticket.get("wait_time", ""),
|
|
})
|
|
return result
|
|
|
|
|
|
def formatInterlinesInfo(interlinesInfo):
|
|
if len(interlinesInfo) == 0:
|
|
return '没有查询到相关的中转车次信息'
|
|
result = '出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 | 换乘等待时间 | 总历时\n\n'
|
|
for info in interlinesInfo:
|
|
result += f"{info['start_date']} {info['start_time']} -> {info['arrive_date']} {info['arrive_time']} | "
|
|
result += f"{info['from_station_name']} -> {info['middle_station_name']} -> {info['end_station_name']} | "
|
|
transfer = '同车换乘' if info['same_train'] else ('同站换乘' if info['same_station'] else '换站换乘')
|
|
result += f"{transfer} | {info['wait_time']} | {info['lishi']}\n\n"
|
|
result += '\t' + formatTicketsInfo(info["ticketList"]).replace('\n', '\n\t') + '\n'
|
|
return result
|
|
|
|
|
|
def parseStationsData(rawData):
|
|
result = {}
|
|
dataArray = rawData.split('|')
|
|
for i in range(0, len(dataArray), 10):
|
|
group = dataArray[i:i + 10]
|
|
if len(group) < 10:
|
|
continue
|
|
station = {}
|
|
for index, key in enumerate(StationDataKeys):
|
|
station[key] = group[index]
|
|
if station.get("station_code"):
|
|
result[station["station_code"]] = station
|
|
return result
|
|
|
|
|
|
async def getStationsInternal():
|
|
stationNameJSUrl = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js"
|
|
stationNameJS = await make12306RequestHtml(stationNameJSUrl)
|
|
if not stationNameJS:
|
|
raise Error('Error: get station name js file content failed.')
|
|
rawDataMatch = re.search(r"var station_names\s*=\s*'(.*?)';", stationNameJS)
|
|
if not rawDataMatch:
|
|
raise Error('Error: could not find station data in JS file.')
|
|
rawData = rawDataMatch.group(1)
|
|
stationsData = parseStationsData(rawData)
|
|
for station in MISSING_STATIONS:
|
|
if station["station_code"] not in stationsData:
|
|
stationsData[station["station_code"]] = station
|
|
return stationsData
|
|
|
|
|
|
async def getLCQueryPath():
|
|
html = await make12306RequestHtml(LCQUERY_INIT_URL)
|
|
if html is None:
|
|
raise Error('Error: get 12306 web page for LCQuery path failed.')
|
|
match = re.search(r"var lc_search_url = '(.+?)'", html)
|
|
if match is None:
|
|
raise Error('Error: get LCQuery path failed.')
|
|
return match.group(1)
|
|
|
|
|
|
async def init():
|
|
global STATIONS, CITY_STATIONS, CITY_CODES, NAME_STATIONS, LCQUERY_PATH, initPromise
|
|
if initPromise is not None:
|
|
return
|
|
if STATIONS is not None:
|
|
return
|
|
try:
|
|
STATIONS = await getStationsInternal()
|
|
LCQUERY_PATH = await getLCQueryPath()
|
|
CITY_STATIONS = {}
|
|
for station in STATIONS.values():
|
|
city = station["city"]
|
|
if city not in CITY_STATIONS:
|
|
CITY_STATIONS[city] = []
|
|
CITY_STATIONS[city].append({"station_code": station["station_code"], "station_name": station["station_name"]})
|
|
CITY_CODES = {}
|
|
for city, stations in CITY_STATIONS.items():
|
|
for station in stations:
|
|
if station["station_name"] == city:
|
|
CITY_CODES[city] = station
|
|
break
|
|
NAME_STATIONS = {}
|
|
for station in STATIONS.values():
|
|
NAME_STATIONS[station["station_name"]] = {"station_code": station["station_code"], "station_name": station["station_name"]}
|
|
except Exception as e:
|
|
initPromise = None
|
|
raise e
|
|
|
|
|
|
async def get_current_date(params):
|
|
now = getCurrentShanghaiDate()
|
|
return formatDate(now)
|
|
|
|
|
|
async def get_stations_code_in_city(params):
|
|
await init()
|
|
if params["city"] not in CITY_STATIONS:
|
|
raise Error('City not found.')
|
|
return CITY_STATIONS[params["city"]]
|
|
|
|
|
|
async def get_station_code_of_citys(params):
|
|
await init()
|
|
result = {}
|
|
for city in params["citys"].split('|'):
|
|
if city not in CITY_CODES:
|
|
result[city] = {"error": "未检索到城市。"}
|
|
else:
|
|
result[city] = CITY_CODES[city]
|
|
return result
|
|
|
|
|
|
async def get_station_code_by_names(params):
|
|
await init()
|
|
result = {}
|
|
for stationName in params["station_names"].split('|'):
|
|
sn = stationName[:-1] if stationName.endswith('站') else stationName
|
|
if sn not in NAME_STATIONS:
|
|
result[stationName] = {"error": "未检索到车站。"}
|
|
else:
|
|
result[stationName] = NAME_STATIONS[sn]
|
|
return result
|
|
|
|
|
|
async def get_station_by_telecode(params):
|
|
await init()
|
|
if params["station_telecode"] not in STATIONS:
|
|
raise Error('Station not found.')
|
|
return STATIONS[params["station_telecode"]]
|
|
|
|
|
|
async def get_tickets(params):
|
|
await init()
|
|
if not checkDate(params["date"]):
|
|
raise Error('The date cannot be earlier than today.')
|
|
if params["from_station"] not in STATIONS or params["to_station"] not in STATIONS:
|
|
raise Error('Station not found.')
|
|
queryParams = {
|
|
'leftTicketDTO.train_date': params["date"],
|
|
'leftTicketDTO.from_station': params["from_station"],
|
|
'leftTicketDTO.to_station': params["to_station"],
|
|
'purpose_codes': 'ADULT',
|
|
}
|
|
queryUrl = f"{API_BASE}/otn/leftTicket/query"
|
|
cookies = await getCookie()
|
|
if not cookies:
|
|
raise Error('Get cookie failed. Check your network.')
|
|
response = await make12306Request(queryUrl, queryParams, {"Cookie": formatCookies(cookies)})
|
|
if not response or not response.get("data") or not response["data"].get("result"):
|
|
raise Error('Get tickets data failed.')
|
|
ticketsData = parseTicketsData(response["data"]["result"])
|
|
ticketsInfo = parseTicketsInfo(ticketsData, response["data"]["map"])
|
|
filteredTicketsInfo = filterTicketsInfo(ticketsInfo, params.get("train_filter_flags") or '', params.get("sort_flag", ''), params.get("sort_reverse", False), params.get("limited_num", 0))
|
|
return formatTicketsInfo(filteredTicketsInfo)
|
|
|
|
|
|
async def get_interline_tickets(params):
|
|
await init()
|
|
if not checkDate(params["date"]):
|
|
raise Error('The date cannot be earlier than today.')
|
|
if params["from_station"] not in STATIONS or params["to_station"] not in STATIONS:
|
|
raise Error('Station not found.')
|
|
cookies = await getCookie()
|
|
if not cookies:
|
|
raise Error('Get cookie failed. Check your network.')
|
|
limited_num = params.get("limited_num") or 10
|
|
interlineData = []
|
|
queryParams = {
|
|
'train_date': params["date"],
|
|
'from_station_telecode': params["from_station"],
|
|
'to_station_telecode': params["to_station"],
|
|
'middle_station': params.get("middle_station") or '',
|
|
'result_index': '0',
|
|
'can_query': 'Y',
|
|
'isShowWZ': 'Y' if params.get("show_wz") else 'N',
|
|
'purpose_codes': '00',
|
|
'channel': 'E',
|
|
}
|
|
while len(interlineData) < limited_num:
|
|
response = await make12306Request(f"{API_BASE}{LCQUERY_PATH}", queryParams, {"Cookie": formatCookies(cookies)})
|
|
if not response:
|
|
raise Error('Request interline tickets data failed.')
|
|
if isinstance(response.get("data"), str):
|
|
return f"很抱歉,未查到相关的列车余票。({response.get('errorMsg')})"
|
|
middleList = response["data"].get("middleList") or []
|
|
interlineData.extend(middleList)
|
|
if response["data"].get("can_query") == 'N' or not middleList:
|
|
break
|
|
queryParams['result_index'] = str(response["data"]["result_index"])
|
|
interlineTicketsInfo = parseInterlinesInfo(interlineData)
|
|
filtered = filterTicketsInfo(interlineTicketsInfo, params.get("train_filter_flags") or '', params.get("sort_flag", ''), params.get("sort_reverse", False), limited_num)
|
|
return formatInterlinesInfo(filtered)
|
|
|
|
|
|
async def get_train_route_stations(params):
|
|
await init()
|
|
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"],
|
|
}
|
|
queryUrl = f"{API_BASE}/otn/czxx/queryByTrainNo"
|
|
cookies = await getCookie()
|
|
if not cookies:
|
|
raise Error('Get cookie failed.')
|
|
response = await make12306Request(queryUrl, queryParams, {"Cookie": formatCookies(cookies)})
|
|
if not response or not response.get("data") or not response["data"].get("data"):
|
|
raise Error('Get train route stations failed.')
|
|
routeStationsInfo = parseRouteStationsInfo(response["data"]["data"])
|
|
if len(routeStationsInfo) == 0:
|
|
return '未查询到相关车次信息。'
|
|
return routeStationsInfo
|
|
|
|
|
|
async def wrap(func, params, successMessage, failMessage):
|
|
try:
|
|
result = await func(params)
|
|
complete({"success": True, "message": successMessage, "data": result})
|
|
except Exception as error:
|
|
console.error(f"Function {func.__name__} failed! Error: {str(error)}")
|
|
import traceback
|
|
complete({"success": False, "message": f"{failMessage}: {str(error)}", "error_stack": traceback.format_exc()})
|
|
|
|
|
|
async def main():
|
|
console.log("--- 开始测试 12306 工具包 ---")
|
|
try:
|
|
await init()
|
|
console.log("\n[1/8] 测试 get_current_date...")
|
|
dateResult = await get_current_date({})
|
|
console.log("测试结果:", json.dumps(dateResult, indent=2, ensure_ascii=False))
|
|
testDate = dateResult
|
|
console.log("\n[2/8] 测试 get_stations_code_in_city (北京)...")
|
|
cityStations = await get_stations_code_in_city({"city": "北京"})
|
|
console.log("测试结果:", json.dumps(cityStations, indent=2, ensure_ascii=False))
|
|
console.log("\n[3/8] 测试 get_station_code_of_citys (北京|上海)...")
|
|
cityCodesResult = await get_station_code_of_citys({"citys": "北京|上海"})
|
|
console.log("测试结果:", json.dumps(cityCodesResult, indent=2, ensure_ascii=False))
|
|
console.log("\n--- 12306 工具包测试完成 ---")
|
|
complete({"success": True, "message": "所有测试已成功或已记录错误。"})
|
|
except Exception as e:
|
|
console.error("测试主函数出现错误:", str(e))
|
|
complete({"success": False, "message": f"测试失败: {str(e)}"})
|
|
|
|
|
|
async def _get_current_date(params):
|
|
await wrap(get_current_date, params, '获取当前日期成功', '获取当前日期失败')
|
|
|
|
|
|
async def _get_stations_code_in_city(params):
|
|
await wrap(get_stations_code_in_city, params, '查询成功', '查询失败')
|
|
|
|
|
|
async def _get_station_code_of_citys(params):
|
|
await wrap(get_station_code_of_citys, params, '查询成功', '查询失败')
|
|
|
|
|
|
async def _get_station_code_by_names(params):
|
|
await wrap(get_station_code_by_names, params, '查询成功', '查询失败')
|
|
|
|
|
|
async def _get_station_by_telecode(params):
|
|
await wrap(get_station_by_telecode, params, '查询成功', '查询失败')
|
|
|
|
|
|
async def _get_tickets(params):
|
|
await wrap(get_tickets, params, '查询余票成功', '查询余票失败')
|
|
|
|
|
|
async def _get_interline_tickets(params):
|
|
await wrap(get_interline_tickets, params, '查询中转票成功', '查询中转票失败')
|
|
|
|
|
|
async def _get_train_route_stations(params):
|
|
await wrap(get_train_route_stations, params, '查询经停站成功', '查询经停站失败')
|
|
|
|
|
|
exports.get_current_date = _get_current_date
|
|
exports.get_stations_code_in_city = _get_stations_code_in_city
|
|
exports.get_station_code_of_citys = _get_station_code_of_citys
|
|
exports.get_station_code_by_names = _get_station_code_by_names
|
|
exports.get_station_by_telecode = _get_station_by_telecode
|
|
exports.get_tickets = _get_tickets
|
|
exports.get_interline_tickets = _get_interline_tickets
|
|
exports.get_train_route_stations = _get_train_route_stations
|
|
exports.main = main
|