#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import http from 'http'; import https from 'https'; import { format, parse } from 'date-fns'; import { toZonedTime } from 'date-fns-tz'; const VERSION = '1.0.0'; const API_BASE = 'https://kyfw.12306.cn'; const WEB_URL = 'https://www.12306.cn/index/'; const SEARCH_API_BASE = 'https://search.12306.cn'; const LEFT_TICKET_INIT = `${API_BASE}/otn/leftTicket/init`; // 全局连接池配置 const GLOBAL_HTTPS_AGENT = new https.Agent({ keepAlive: true, maxSockets: 30, maxFreeSockets: 15, timeout: 20000, scheduling: 'fifo' }); const GLOBAL_HTTP_AGENT = new http.Agent({ keepAlive: true, maxSockets: 30, maxFreeSockets: 15, timeout: 20000, scheduling: 'fifo' }); // 通用网络请求重试函数 async function makeRequestWithRetry( requestFn: () => Promise, maxRetries: number = 3, baseDelay: number = 300 ): Promise { let lastError: any = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await requestFn(); } catch (error: any) { lastError = error; const msg = String(error); const errorCode = error?.code; // 检查是否为瞬态错误 const transient = msg.includes('socket hang up') || msg.includes('ECONNRESET') || msg.includes('ETIMEDOUT') || msg.includes('EAI_AGAIN') || msg.includes('ECONNABORTED') || msg.includes('ENOTFOUND') || msg.includes('ECONNREFUSED') || msg.includes('EPIPE') || msg.includes('EHOSTUNREACH') || msg.includes('ENETUNREACH') || msg.includes('EADDRNOTAVAIL') || msg.includes('timeout') || msg.includes('Network Error') || errorCode === 'ECONNRESET' || errorCode === 'ETIMEDOUT' || errorCode === 'ECONNABORTED' || errorCode === 'ENOTFOUND' || errorCode === 'ECONNREFUSED' || errorCode === 'EPIPE' || errorCode === 'EHOSTUNREACH' || errorCode === 'ENETUNREACH'; if (transient && attempt < maxRetries) { const delay = Math.min(baseDelay * Math.pow(1.5, attempt), 3000); const jitter = Math.floor(Math.random() * 200); const waitMs = delay + jitter; console.error(`网络请求重试 ${attempt + 1}/${maxRetries}, 错误: ${msg}, 等待 ${waitMs}ms`); await new Promise((r) => setTimeout(r, waitMs)); continue; } throw error; } } throw lastError; } // 简化的类型定义 interface 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; } // 全局变量 let STATIONS: Record = {}; let CITY_STATIONS: Record = {}; let CITY_CODES: Record = {}; let NAME_STATIONS: Record = {}; /** * 12306火车票查询 MCP 服务器 */ class TicketSearchMCPServer { private server: Server; constructor() { this.server = new Server( { name: '12306-ticket-search-mcp', version: VERSION, }, { capabilities: { tools: {}, }, } ); this.setupTools(); } private async initializeStations() { if (Object.keys(STATIONS).length > 0) { return; // 已经初始化过了 } try { // 获取车站数据 - 使用重试机制 const html = await makeRequestWithRetry(() => axios.get(WEB_URL, { httpsAgent: GLOBAL_HTTPS_AGENT, timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Cache-Control': 'no-cache' } }) ); const match = html.data.match('.(/script/core/common/station_name.+?.js)'); if (!match) { throw new Error('无法获取车站数据'); } const stationNameJS = await makeRequestWithRetry(() => axios.get(new URL(match[0], WEB_URL).href, { httpsAgent: GLOBAL_HTTPS_AGENT, timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Cache-Control': 'no-cache' } }) ); const rawData = eval(stationNameJS.data.replace('var station_names =', '')); // 解析车站数据 const dataArray = rawData.split('|'); const dataList: string[][] = []; for (let i = 0; i < Math.floor(dataArray.length / 10); i++) { dataList.push(dataArray.slice(i * 10, i * 10 + 10)); } for (const group of dataList) { const station: Partial = {}; const keys = ['station_id', 'station_name', 'station_code', 'station_pinyin', 'station_short', 'station_index', 'code', 'city', 'r1', 'r2']; keys.forEach((key, index) => { station[key as keyof StationData] = group[index]; }); if (station.station_code) { STATIONS[station.station_code] = station as StationData; } } // 构建城市索引 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, }); // 城市代表车站 if (!CITY_CODES[city] && station.station_name === city) { CITY_CODES[city] = { station_code: station.station_code, station_name: station.station_name, }; } // 车站名索引 NAME_STATIONS[station.station_name] = { station_code: station.station_code, station_name: station.station_name, }; } } catch (error) { console.error('初始化车站数据失败:', error); throw new McpError( ErrorCode.InternalError, '无法初始化车站数据,请检查网络连接' ); } } private setupTools() { // 注册工具列表处理器 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get-stations-code-in-city', description: '通过中文城市名查询该城市所有火车站的名称及其对应的station_code', inputSchema: { type: 'object', properties: { city: { type: 'string', description: '中文城市名称,例如:"北京", "上海"' } }, required: ['city'] } }, { name: 'get-station-code-of-citys', description: '通过中文城市名查询代表该城市的station_code', inputSchema: { type: 'object', properties: { citys: { type: 'string', description: '要查询的城市,比如"北京"。若要查询多个城市,可以用逗号(,)或竖线(|)分割,比如"北京,上海"或"北京|上海"。' } }, required: ['citys'] } }, { name: 'get-station-code-by-names', description: '通过具体的中文车站名查询其station_code和车站名', inputSchema: { type: 'object', properties: { stationNames: { type: 'string', description: '具体的中文车站名称,例如:"北京南", "上海虹桥"。若要查询多个站点,可以用逗号(,)或竖线(|)分割,比如"北京南,上海虹桥"或"北京南|上海虹桥"。' } }, required: ['stationNames'] } }, { name: 'get-tickets', description: '查询12306余票信息', inputSchema: { type: 'object', properties: { date: { type: 'string', description: '查询日期,格式为 "yyyy-MM-dd"。' }, fromStation: { type: 'string', description: '出发地的station_code' }, toStation: { type: 'string', description: '到达地的station_code' }, trainFilterFlags: { type: 'string', description: '车次筛选条件,可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]' }, earliestStartTime: { type: 'number', description: '最早出发时间(0-24),默认为0' }, latestStartTime: { type: 'number', description: '最迟出发时间(0-24),默认为24' }, sortFlag: { type: 'string', description: '排序方式,可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]' }, sortReverse: { type: 'boolean', description: '是否逆向排序结果,默认为false' }, limitedNum: { type: 'number', description: '返回的余票数量限制,默认为0,即不限制' }, timeoutMs: { type: 'number', description: '请求超时时间(毫秒),默认12000,可增大以适配不稳定网络' }, retriesPerEndpoint: { type: 'number', description: '每端点重试次数,默认3,增大可提升成功率' }, endpointOrder: { type: 'string', description: '端点顺序,逗号分隔,例如"Z,X,A,H,I,O,N",将按顺序尝试' }, preferSingleEndpoint: { type: 'boolean', description: '是否仅使用单一端点(减少端点切换),默认false' }, singleEndpoint: { type: 'string', description: '当preferSingleEndpoint为true时使用的端点:Z/X/A/H/I/O/N,默认Z' } }, required: ['date', 'fromStation', 'toStation'] } }, { name: 'get-train-route-stations', description: '查询指定车次在指定日期的经停站明细(到/出发时间、停留时长等),支持 text/json 输出', inputSchema: { type: 'object', properties: { trainCode: { type: 'string', description: '车次代码,例如 "G1033"' }, departDate: { type: 'string', description: '发车日期,格式 "yyyy-MM-dd"' }, format: { type: 'string', description: '返回格式,text 或 json,默认 text' } }, required: ['trainCode', 'departDate'] } } ] }; }); // 注册工具调用处理器 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: rawArgs } = request.params; // 处理参数 let args: any; if (typeof rawArgs === 'string') { try { args = JSON.parse(rawArgs); } catch (error) { throw new McpError( ErrorCode.InvalidParams, '参数格式错误:无法解析参数JSON' ); } } else { args = rawArgs; } try { // 确保车站数据已初始化 await this.initializeStations(); switch (name) { case 'get-stations-code-in-city': return await this.handleGetStationsCodeInCity(args); case 'get-station-code-of-citys': return await this.handleGetStationCodeOfCitys(args); case 'get-station-code-by-names': return await this.handleGetStationCodeByNames(args); case 'get-tickets': return await this.handleGetTickets(args); case 'get-train-route-stations': return await this.handleGetTrainRouteStations(args); default: throw new McpError( ErrorCode.MethodNotFound, `未知的工具: ${name}` ); } } catch (error) { console.error(`工具 '${name}' 执行错误:`, error); throw new McpError( ErrorCode.InternalError, `工具执行失败: ${error instanceof Error ? error.message : String(error)}` ); } }); } private async handleGetStationsCodeInCity(args: any) { const { city } = args; if (!(city in CITY_STATIONS)) { return { content: [{ type: 'text', text: 'Error: City not found.' }], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify(CITY_STATIONS[city]) } ], isError: false, }; } private async handleGetStationCodeOfCitys(args: any) { const { citys } = args; let result: Record = {}; for (const city of citys.split(/[|,]/)) { const trimmedCity = city.trim(); if (!(trimmedCity in CITY_CODES)) { result[trimmedCity] = { error: '未检索到城市。' }; } else { result[trimmedCity] = CITY_CODES[trimmedCity]; } } return { content: [{ type: 'text', text: JSON.stringify(result) }], isError: false, }; } private async handleGetStationCodeByNames(args: any) { const { stationNames } = args; let result: Record = {}; for (let stationName of stationNames.split(/[|,]/)) { stationName = stationName.trim().endsWith('站') ? stationName.trim().substring(0, -1) : stationName.trim(); if (!(stationName in NAME_STATIONS)) { result[stationName] = { error: '未检索到车站。' }; } else { result[stationName] = NAME_STATIONS[stationName]; } } return { content: [{ type: 'text', text: JSON.stringify(result) }], isError: false, }; } private async handleGetTrainRouteStations(args: any) { const { trainCode, departDate, format = 'text' } = args; // 基本校验 if (!trainCode || !departDate) { return { content: [{ type: 'text', text: 'Error: missing params trainCode/departDate.' }], isError: true, }; } if (!this.checkDate(departDate)) { return { content: [{ type: 'text', text: 'Error: The date cannot be earlier than today.' }], isError: true, }; } try { // 1) 查 train_no - 使用重试机制 const sp = new URLSearchParams({ keyword: String(trainCode), date: String(departDate).replace(/-/g, ''), }); const searchUrl = `${SEARCH_API_BASE}/search/v1/train/search?${sp.toString()}`; const searchResp = await makeRequestWithRetry(() => axios.get(searchUrl, { headers: { Accept: 'application/json, text/plain, */*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Cache-Control': 'no-cache' }, httpsAgent: GLOBAL_HTTPS_AGENT, timeout: 15000 }) ); const searchData = (searchResp.data && searchResp.data.data) ? searchResp.data.data : []; if (!Array.isArray(searchData) || searchData.length === 0) { return { content: [{ type: 'text', text: 'Error: train not found.' }], isError: true, }; } const trainNo = searchData[0].train_no; // 2) 携带 Cookie 请求经停站 - 使用重试机制 const initUrl = `${API_BASE}/otn/queryTrainInfo/init`; const initResp = await makeRequestWithRetry(() => axios.get(initUrl, { headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', Origin: 'https://kyfw.12306.cn', 'Accept-Language': 'zh-CN,zh;q=0.9', 'X-Requested-With': 'XMLHttpRequest', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Cache-Control': 'no-cache' }, httpsAgent: GLOBAL_HTTPS_AGENT, timeout: 15000 }) ); const setCookies = initResp.headers['set-cookie'] as unknown as string[] | undefined; const cookies = (setCookies && setCookies.length) ? setCookies.map(c => c.split(';')[0]).join('; ') : ''; if (!cookies) { return { content: [{ type: 'text', text: 'Error: get cookie failed.' }], isError: true, }; } const qp = new URLSearchParams({ 'leftTicketDTO.train_no': trainNo, 'leftTicketDTO.train_date': departDate, rand_code: '', }); const queryUrl = `${API_BASE}/otn/queryTrainInfo/query?${qp.toString()}`; const queryResp = await makeRequestWithRetry(() => axios.get(queryUrl, { headers: { Accept: 'application/json, text/plain, */*', Cookie: cookies, Referer: `${API_BASE}/otn/queryTrainInfo/init`, Origin: 'https://kyfw.12306.cn', 'Accept-Language': 'zh-CN,zh;q=0.9', 'X-Requested-With': 'XMLHttpRequest', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', Host: 'kyfw.12306.cn', Connection: 'keep-alive', 'Cache-Control': 'no-cache' }, withCredentials: true, httpsAgent: GLOBAL_HTTPS_AGENT, timeout: 15000 }) ); const data = (queryResp.data?.data?.data) ?? (queryResp.data?.data) ?? []; if (!Array.isArray(data) || data.length === 0) { const diag = { httpstatus: queryResp.data?.httpstatus, errorMsg: queryResp.data?.errorMsg, messages: queryResp.data?.messages, raw: typeof queryResp.data === 'string' ? queryResp.data.slice(0, 500) : undefined, }; return { content: [{ type: 'text', text: `Error: get train route stations failed. ${JSON.stringify(diag)}` }], isError: true, }; } if (String(format).toLowerCase() === 'json') { return { content: [{ type: 'text', text: JSON.stringify(data) }], isError: false, }; } else { // 简单文本格式化(保守) const lines = data.map((s: any) => { const name = s.station_name; const code = s.station_train_code; const arrive = s.arrive_time; const start = s.start_time; const lishi = s.running_time || s.lishi || ''; return `${code} | ${name} | arrive ${arrive} | start ${start} | duration ${lishi}`; }); return { content: [{ type: 'text', text: lines.join('\n') }], isError: false, }; } } catch (err) { return { content: [{ type: 'text', text: `Error: ${err}`}], isError: true, }; } } private async handleGetTickets(args: any) { const { date, fromStation, toStation, trainFilterFlags = '', earliestStartTime = 0, latestStartTime = 24, sortFlag = '', sortReverse = false, limitedNum = 0, timeoutMs = 15000, retriesPerEndpoint = 3 } = args; // 检查日期是否早于当前日期 if (!this.checkDate(date)) { return { content: [{ type: 'text', text: 'Error: The date cannot be earlier than today.' }], isError: true, }; } if (!Object.keys(STATIONS).includes(fromStation) || !Object.keys(STATIONS).includes(toStation)) { return { content: [{ type: 'text', text: 'Error: Station not found.' }], isError: true, }; } // 创建专用的 HTTPS Agent 用于连接池管理 const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 20, maxFreeSockets: 10, timeout: 30000, }); const params = new URLSearchParams({ 'leftTicketDTO.train_date': date, 'leftTicketDTO.from_station': fromStation, 'leftTicketDTO.to_station': toStation, purpose_codes: 'ADULT', }); // 多端点重试策略 const endpoints = ['queryZ', 'queryX', 'queryA', 'queryH', 'queryI', 'queryO', 'queryN']; let lastError: any = null; for (const endpoint of endpoints) { for (let attempt = 0; attempt < retriesPerEndpoint; attempt++) { try { const cookies = await getCookie(); if (!cookies) { throw new Error('获取 Cookie 失败'); } const url = `${API_BASE}/otn/leftTicket/${endpoint}?${params.toString()}`; const resp = await axios.get(url, { headers: { Accept: 'application/json, text/plain, */*', Cookie: cookies, Referer: LEFT_TICKET_INIT, Origin: 'https://kyfw.12306.cn', 'Accept-Language': 'zh-CN,zh;q=0.9', 'X-Requested-With': 'XMLHttpRequest', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', Host: 'kyfw.12306.cn', Connection: 'keep-alive', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }, withCredentials: true, timeout: timeoutMs, httpsAgent, maxRedirects: 3, validateStatus: (status) => status >= 200 && status < 400, }); if (!resp.data || !resp.data.data) { throw new Error(`端点 ${endpoint} 返回数据格式错误`); } const result = resp.data.data.result || []; // 应用过滤和排序逻辑(如果需要) let filteredResult = result; // 车次类型过滤 if (trainFilterFlags) { const flags = trainFilterFlags.toUpperCase().split(''); filteredResult = result.filter((train: any) => { const trainCode = train.split('|')[3] || ''; return flags.some((flag: string) => { switch (flag) { case 'G': return trainCode.startsWith('G') || trainCode.startsWith('C'); case 'D': return trainCode.startsWith('D'); case 'Z': return trainCode.startsWith('Z'); case 'T': return trainCode.startsWith('T'); case 'K': return trainCode.startsWith('K'); case 'F': return trainCode.includes('复兴号'); case 'S': return trainCode.includes('智能'); case 'O': return !['G', 'C', 'D', 'Z', 'T', 'K'].some(prefix => trainCode.startsWith(prefix)); default: return false; } }); }); } // 时间过滤 if (earliestStartTime > 0 || latestStartTime < 24) { filteredResult = filteredResult.filter((train: any) => { const startTime = train.split('|')[8] || ''; if (!startTime) return true; const hour = parseInt(startTime.split(':')[0] || '0'); return hour >= earliestStartTime && hour <= latestStartTime; }); } // 排序 if (sortFlag) { filteredResult.sort((a: any, b: any) => { const aData = a.split('|'); const bData = b.split('|'); let comparison = 0; switch (sortFlag) { case 'startTime': comparison = (aData[8] || '').localeCompare(bData[8] || ''); break; case 'arriveTime': comparison = (aData[9] || '').localeCompare(bData[9] || ''); break; case 'duration': comparison = (aData[10] || '').localeCompare(bData[10] || ''); break; } return sortReverse ? -comparison : comparison; }); } // 限制数量 if (limitedNum > 0) { filteredResult = filteredResult.slice(0, limitedNum); } return { content: [{ type: 'text', text: JSON.stringify(filteredResult) }], isError: false, }; } catch (error: any) { lastError = error; const msg = String(error); const errorCode = error?.code; // 检查是否为瞬态错误 const transient = msg.includes('socket hang up') || msg.includes('ECONNRESET') || msg.includes('ETIMEDOUT') || msg.includes('EAI_AGAIN') || msg.includes('ECONNABORTED') || msg.includes('ENOTFOUND') || msg.includes('ECONNREFUSED') || msg.includes('EPIPE') || msg.includes('EHOSTUNREACH') || msg.includes('ENETUNREACH') || msg.includes('EADDRNOTAVAIL') || msg.includes('timeout') || msg.includes('Network Error') || errorCode === 'ECONNRESET' || errorCode === 'ETIMEDOUT' || errorCode === 'ECONNABORTED' || errorCode === 'ENOTFOUND' || errorCode === 'ECONNREFUSED' || errorCode === 'EPIPE' || errorCode === 'EHOSTUNREACH' || errorCode === 'ENETUNREACH'; if (transient && attempt < retriesPerEndpoint - 1) { // 指数退避重试 const baseDelay = Math.min(300 * Math.pow(1.5, attempt), 2000); const jitter = Math.floor(Math.random() * 200); const waitMs = baseDelay + jitter; console.error(`端点 ${endpoint} 重试 ${attempt + 1}/${retriesPerEndpoint}, 错误: ${msg}, 等待 ${waitMs}ms`); await new Promise((r) => setTimeout(r, waitMs)); continue; } // 非瞬态错误或重试次数用完,尝试下一个端点 console.error(`端点 ${endpoint} 失败,尝试次数: ${attempt + 1}, 错误: ${msg}`); break; } } } // 所有端点都失败了 console.error('所有端点都失败了,最后错误:', lastError); return { content: [{ type: 'text', text: `Error: ${lastError?.message || lastError || 'Network request failed after all retries'}` }], isError: true, }; } private checkDate(date: string): boolean { const timeZone = 'Asia/Shanghai'; const nowInShanghai = toZonedTime(new Date(), timeZone); nowInShanghai.setHours(0, 0, 0, 0); const inputInShanghai = toZonedTime(new Date(date), timeZone); inputInShanghai.setHours(0, 0, 0, 0); return inputInShanghai >= nowInShanghai; } /** * 启动服务器 */ async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('12306火车票查询 MCP 服务器已启动'); // 捕获未处理错误 process.on('uncaughtException', (err) => { console.error('未捕获异常:', err); }); process.on('unhandledRejection', (reason) => { console.error('未处理的 Promise 拒绝:', reason); }); } } // 启动服务器 async function main() { try { const server = new TicketSearchMCPServer(); await server.start(); } catch (error) { console.error('服务器启动失败:', error); process.exit(1); } } // 启动服务器 main().catch((error) => { console.error('服务器运行错误:', error); process.exit(1); }); // 稳定 Cookie 获取:访问 init 页面,多次重试,启用 KeepAlive,拼接 Set-Cookie 的 k=v async function getCookie(): Promise { const maxRetries = 5; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const resp = await makeRequestWithRetry(() => axios.get(LEFT_TICKET_INIT, { headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9', Referer: 'https://kyfw.12306.cn/otn/leftTicket/init', Origin: 'https://kyfw.12306.cn', Connection: 'keep-alive', Host: 'kyfw.12306.cn', 'Accept-Encoding': 'gzip, deflate', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }, timeout: 15000, httpAgent: GLOBAL_HTTP_AGENT, httpsAgent: GLOBAL_HTTPS_AGENT, maxRedirects: 5, validateStatus: (s) => s >= 200 && s < 400, }) ); const setCookies = resp.headers['set-cookie'] as unknown as string[] | undefined; if (setCookies && setCookies.length > 0) { const cookieString = setCookies.map((c) => c.split(';')[0]).join('; '); if (cookieString.trim()) { return cookieString; } } // 没拿到有效 Cookie,短暂等待后重试 if (attempt < maxRetries) { const waitMs = 300 * (attempt + 1) + Math.floor(Math.random() * 200); await new Promise((r) => setTimeout(r, waitMs)); } } catch (e) { console.error(`getCookie 尝试 ${attempt + 1}/${maxRetries + 1} 失败:`, e); if (attempt === maxRetries) { throw e; } } } console.error(`getCookie 最终失败,尝试次数: ${maxRetries + 1}`); return ''; }