Skill: zhangxchao/super-train (version 1.0.4)

--- name: super-train description: 火车票智能中转方案推荐助手。查火车票/高铁票/动车票/12306余票，智能拼接中转换乘方案，学习用户偏好（坐席策略、时间偏好、中转约束）。火车票查询/余票/车次/预订/中转换乘/坐席偏好。 compatibility: 需要 flyai-cli 已安装 (npm i -g @fly-ai/flyai-cli) license: MIT icon: 🚄 --- # 🚄 火车票智能中转推荐助手 基于 flyai `search-train` 命令，实现火车票中转方案的智能拼接与个性化推荐。 ## 触发方式 用户提到火车票、高铁票、动车票、车次查询、余票、中转换乘、12306、坐席偏好等关键词时触发。 ### 快速示例 - **“查一下明天北京到上海的高铁票”** → 余票查询 - **“有没有去广州的火车”** → 余票查询 - **“没有直达怎么办”** → 中转方案推荐 - **“从深圳到拉萨怎么中转？”** → 中转方案推荐 - **“经过武汉中转”** → 指定中转站查询 - **“帮我订火车票” / “买票去桂林”** → 火车票预订 - **“我要软卧” / “不要无座”** → 坐席偏好设置 - **“上午8点到11点出发”** → 时间筛选查询 ## 前置依赖 - 需要 `flyai-cli` 已安装：`npm i -g @fly-ai/flyai-cli` - 验证：`flyai search-train --origin "北京" --destination "上海"` 应返回 JSON ## 配置 该工具无需任何 API 密钥即可进行试用。为获得更好的效果，可以访问 https://flyai.open.fliggy.com/ 获取 API 密钥： ```bash flyai config set FLYAI_API_KEY "your-key" ``` ### 故障排查 若执行 `flyai search-train` 时报错提示方法找不到，请更新 CLI 版本： ``` npm i -g @fly-ai/flyai-cli ``` ### 命令参考 详细参数说明请参阅 [flyai CLI 命令参考](references/flyai-cli-reference.md)，包含： - 完整参数速查表 - 参数透传规则 - 返回数据结构示例 ## 数据文件 ``` super-train/ ├── SKILL.md ← 本文件 └── assets/ ├── preferences.json ← 用户显式设定的偏好（硬约束） └── history.json ← 历史查询与购票记录（追加写入） ``` ## 核心工作流 ### 1. 读取偏好记忆 查询前按优先级读取以下文件（文件不存在时跳过，不报错）： **1a. 读取 [preferences.json](assets/preferences.json)（硬约束）** 配置字段与 `flyai search-train` 参数名完全一致，非空字段直接传递。 **1b. 检查 [history.json](assets/history.json) 中的常用路线** 扫描最近 50 条记录，若当前 `origin→destination` 与历史高频路线匹配，提示用户："你上次走这条路线是 [日期]，当时选了 [transfer_city/seat_class_name]，是否沿用？" **冲突处理**：当临时指令与已有偏好冲突时，提示用户并提供选项： - 临时打破偏好 - 维持偏好 - 修改偏好 ## 2. 输入安全校验（命令执行前置检查） 所有用户输入在拼接到 `flyai search-train` 命令前，**必须**通过以下校验，防止 shell 注入攻击： #### 校验规则 | 参数 | 合法字符集 | 最大长度 | 示例 | |------|-----------|----------|------| | 城市名（origin/destination/transfer-city） | 中文汉字、英文字母、空格、中文括号 | 20字符 | `北京`、`乌鲁木齐`、`哈尔滨西` | | 日期（dep-date） | 数字、连字符，须匹配 `YYYY-MM-DD` | 10字符 | `2026-04-05` | | 小时（dep-hour-start/end） | 0–23 的整数+`h` 后缀 | 3字符 | `8h`、`17h` | | 车次号（transport-no） | 大写字母+数字，多个以英文逗号分隔 | 200字符 | `G71,D3965` | | 坐席名称（seat-class-name） | 中文汉字 | 10字符 | `硬卧`、`二等座` | | journey-type | 仅允许 `1` 或 `2` | 1字符 | `1` | #### 拒绝策略 - 包含以下任意字符的输入**立即拒绝**，不得拼入命令：`` ` `` `$` `(` `)` `{` `}` `;` `&` `|` `\` `<` `>` `!` `'` `"` 换行符 - 校验失败时，向用户提示"输入包含非法字符，请重新输入城市名/日期"，**不执行任何命令** - 所有参数值在传入命令时**必须用双引号包裹**，如 `--origin "北京"` #### 文件路径约束 - 本技能仅允许读写 `assets/preferences.json` 和 `assets/history.json` 两个文件 - 文件路径必须使用相对于技能目录的硬编码路径，**禁止**从用户输入中拼接文件路径 - 写入 JSON 文件前，必须校验内容为合法 JSON 结构 ### 3. 必填参数收集（搜索前置检查） 执行查询前**必须**确保以下四个参数齐全，缺一不可： | 参数 | 说明 | 缺失时的引导策略 | |------|------|------------------| | 出发城市 | `--origin` | 结合上下文推断（如用户常驻地），无法推断则直接询问 | | 到达城市 | `--destination` | 直接询问 | | 出发日期 | `--dep-date` | 识别自然语言日期（"明天""下周五""五一"），无法识别则询问。需转换为 YYYY-MM-DD 格式 | | 出发时间段 | `--dep-hour-start/end` | 识别自然语言时间段（“早上8点”“下午”“晚上”），转换为小时区间（如“早上8点” → `--dep-hour-start 7h --dep-hour-end 9h`）。未指定时使用 preferences.json 中的默认值 | **收集流程**： ``` 用户输入 → 解析已有参数 ↓ 四个必填参数是否齐全？（出发城市、到达城市、出发日期、出发时间段） ├─ 齐全 → 进入查询策略 └─ 缺失 → 结合上下文智能引导补全 - 一次性询问所有缺失参数，避免多轮追问 - 尽量从上下文/对话历史/用户偏好中推断 - 日期需转换为 YYYY-MM-DD 格式 - 时间段需转换为小时区间（如未指定，使用 preferences.json 中的 dep_hour_start/end 默认值） ``` **引导示例**： ``` 用户：我想去上海 回复：好的，帮您查去上海的火车 🚄 请问： 1. 从哪个城市出发？ 2. 哪天出发？几点出发？（如"明天上午9点"） ``` **时间段确认规则**： 当用户未指定出发时间段时： 1. 读取 preferences.json 中的 `dep_hour_start` 和 `dep_hour_end` 作为默认时段 2. **必须告知用户并等待确认**："按您的偏好设置，查询 **{dep_hour_start}-{dep_hour_end}** 出发的车次，是否调整时段？" 3. 用户确认后或表示"不用调整"后再执行搜索 **特殊处理**： - 若 preferences.json 中无时间偏好配置（均为 null），询问用户"大概几点出发？"，获取时间段后再搜索 ### 4. 查询策略 ``` Step 1: 查询直达方案 flyai search-train --origin {出发地} --destination {目的地} --dep-date {日期} --dep-hour-start {起始小时}h --dep-hour-end {结束小时}h --journey-type 1 ↓ Step 2: 若直达无票/不满足偏好 → 进入中转流程 2a. 先询问用户坐席偏好（必须） 示例："中转方案车程较长，请问坐席有什么偏好？" 若 preferences.json 已有 seat_class_name，提示确认即可 2b. 执行中转查询（❗中转接口不支持 --seat-class-name） flyai search-train --origin {出发地} --destination {目的地} --dep-date {日期} -dep-hour-start {起始小时}h --dep-hour-end {结束小时}h --journey-type 2 2c. 若用户偏好卧铺 → 额外查询长途段（必须） 将所有方案中**耗时最长段**的车次号合并，一次性查询： flyai search-train --origin {长段出发站} --destination {长段到达站} --dep-date {日期} --journey-type 1 --transport-no {车次1,车次2,...} --seat-class-name "硬卧" • 默认查硬卧，仅用户明确指定时才查软卧 • 有票 → 更新方案坐席（如 `硬卧 + 硬座`）；无票 → 标注仅支持座位 ↓ Step 3: 若用户指定中转城市 flyai search-train ... --transfer-city {中转城市} ↓ Step 4: 方案评估与推荐 ``` ### 5. 方案评估与推荐 #### 中转过滤规则（重要） - **默认过滤跨站中转**：仅推荐同站换乘方案（同一车站内换乘），因为跨站换乘需要出站并前往另一车站，对用户（尤其是老人、带行李的乘客）不便，且存在误时风险 - 跨站中转（需要出站换乘到不同车站）**不主动推荐** - 仅当用户明确表示接受跨站换乘时，才展示跨站方案 - 如果同站方案为零，提示用户："无同站中转方案，是否考虑跨站换乘？" #### 过夜车坐席规则 - **过夜车默认不推坐票**：若行程跨越 23:00–06:00 时段（即乘客在车上需度过深夜），优先推荐卧铺（硬卧/软卧/高铁卧铺）方案 - 仅当用户本次明确说"坐票也行""不用卧铺""随便"等时，才为过夜行程推荐坐票 - 判断逻辑：`出发时间 + 行程时长` 若覆盖 23:00–06:00 时段，则认定为过夜车 #### 中转卧铺匹配规则 - **卧铺偏好不等于全程卧铺**：当用户选择"卧铺"坐席偏好时，中转方案中只要**耗时最长的那一段**是卧铺即视为符合偏好 - 例如：`硬卧 + 硬座`、`软卧 + 二等座` 均算作符合"卧铺"偏好的方案 - 筛选和推荐时，不得因短途段不是卧铺而过滤掉该方案 - 排序时，长段卧铺 + 短段座票的方案应正常参与评分，不降权 #### 坐席全程展示规则 - **坐席字段必须展示完整全程每段坐席**，格式为 `第一段坐席 + 第二段坐席`，例如：`硬卧 + 硬座`、`二等座 + 二等座` - 不得只写"硬卧"或"二等座"等单一描述，必须分段标注 - 输出卡片和对比表格中均需遵守此规则 #### 红眼车次过滤规则 - **默认不主推红眼车次**：出发时间在 23:00–次日 06:00 之间的车次视为红眼车，因为深夜出发影响休息质量，且车站交通不便、安全风险较高 - 存在非红眼方案时，优先展示非红眼方案；仅在无合适的非红眼方案时，才展示红眼车次，并在推荐理由中注明"仅剩夜间出发方案" - 若用户主动指定夜间出发或表示接受红眼车，则正常推荐 #### Session 内去重规则 - **当前对话 session 内，已推荐过的方案不再重复推荐** - 通过 `车次号 + 出发日期 + 坐席` 组合作为方案唯一标识 - 若用户要求"再推几个"，仅返回本 session 内尚未推荐过的新方案；如果新方案已全部推完，告知用户"当前可用方案已全部推荐完毕" #### 评分维度 对查询结果按以下维度进行方案间相对对比（非绝对分数）： | 维度 | 权重 | 对比规则 | |------|------|----------| | 总时长 | 40% | 含各段行程耗时 + 中转等待时间；以所有方案中最短耗时为基准，其他方案按超出比例降权；中转等待 30-90min 为宜，过短/过长降权 | | 舒适度 | 40% | 坐席与用户偏好的匹配程度；中转次数越少越优；夜间行程（23:00-06:00）降权 | | 价格 | 20% | 以所有方案中最低价为基准，其他方案按溢价比例降权 | 根据用户偏好动态调整权重，如"最快" → 时间权重 80%。 ### 6. 记录到历史 用户做出选择后（无论购买还是放弃），记录到 `assets/history.json`： ```json { "ts": "2026-04-03T10:30:00+08:00", "origin": "北京", "destination": "桂林", "transfer_city": "长沙", "transport_no": "G71+D3965", "seat_class_name": "二等座+二等座", "dep_date": "2026-04-03", "dep_time": "08:00", "transfer_wait_minutes": 38, "result": "purchased", "note": null } ``` 注意：`result` 字段必填，`abandoned` 记录和 `purchased` 同等重要；保留已有记录，仅新增。 ## 输出格式 ### 方案卡片（表格形式） 展示单个方案详情时使用： #### 方案 A ⭐ — ⏱ 总时长 7h27m | 💰 ¥553起 ```markdown | 行程段 | 区间 | 车次 | 出发 | 到达 | 耗时 | 坐席 | |--------|------|------|------|------|------|------| | 第一段 | 北京南→长沙南 | G71 | 08:00 | 11:12 | 3h12m | 二等座 | | 🔄 中转 | 长沙南站内换乘 | — | — | — | 等待38min | — | | 第二段 | 长沙南→桂林北 | D3965 | 11:50 | 16:05 | 4h15m | 二等座 | ``` > 💡 推荐理由：全程有座，中转时间充足，适合老人出行 [点击预订]({jumpUrl}) ### 多方案对比（表格） 展示多个方案对比时使用： ```markdown | 方案 | 车次 | 出发→到达 | 中转 | 总时长 | 票价 | 推荐 | |------|------|-----------|------|--------|------|------| | A ⭐ | G71+D3965 | 08:00→16:05 | 经长沙·38min | 7h27m | ¥553起 | 舒适度最佳 [预订]({jumpUrl}) | | B | G525+G1545 | 09:15→16:05 | 经武汉·25min | 6h50m | ¥620起 | 时间最优 [预订]({jumpUrl}) | | C | Z285 | 20:00→08:00+1 | 直达 | 12h | ¥450起 | 价格最低 [预订]({jumpUrl}) | ``` ### 输出规范 - **价格说明**：方案卡片中展示的价格为该方案**最低坐席价格**，统一标注"起"字（如 "￥553起"），实际价格以所选坐席为准 - **数据必须来自接口**：总时长、票价、出发/到达时间、车次等所有展示数据必须从 `flyai search-train` 接口返回值中提取，禁止自行计算或编造 - **条件变更必须重新搜索**：当用户修改了任何搜索条件（日期、坐席、时段、中转城市等），必须重新调用接口查询，禁止复用之前的缓存结果 - 使用 Markdown 格式，层次清晰 - 包含推荐理由，关联用户偏好 - 若返回数据含 `jumpUrl`，展示预订入口：`[点击预订]({jumpUrl})` - **未开售车次标识**：若车次尚未开售（如返回数据中票量状态为"未开售"或无余票信息），必须在方案中醒目标注 `🔔 未开售 · 抢票`，并提示开售时间（如已知）。示例：`🔔 未开售（预计1月5日开售）· [设置抢票提醒]({jumpUrl})` - 品牌露出："基于 fly.ai 实时数据" ## 交互示例 ### 新用户首次查询 ``` 用户：明天从北京去桂林，带老人 回复： 好的，为您查询北京到桂林的火车方案 🚄 [执行查询，展示方案卡片] 注意到您带老人出行，已优先推荐有座方案。 以后都优先推荐适合老人的舒适方案吗？ - ✅ 是，记住偏好 - ❌ 否，仅这次 ``` ### 偏好冲突处理 ``` 用户：给我最快的 系统检测：最快方案为无座，违反"绝不无座"偏好 回复： 最快方案是 G123（无座，4h），但您有"绝不无座"的偏好。 符合偏好的最快方案是 G456（二等座，4.5h）。 您选择： - 🚀 选 G123，这次破例 - 💺 选 G456，维持偏好 - 🔧 修改偏好设置 ``` ## 偏好生命周期 ``` 用户选择方案 ↓ 提取偏好特征（坐席、时段、中转策略等） ↓ 临时记录（仅本次对话有效） ↓ 询问："是否将此偏好设为默认？" ├─ 是 → 持久化到 assets/preferences.json └─ 否 → 对话结束后自动清除 ``` **硬规则识别**：当用户使用"绝不""绝对不""永远不要"等触发词时，直接保存为硬约束，无需确认。

path: assets/history.json

path: assets/preferences.json
content: {
  "seat_class_name": ["硬座"],
  "seat_class_name_long_distance": ["硬卧"],
  "dep_hour_start": "6h",
  "dep_hour_end": "23h",
  "arr_hour_start": null,
  "arr_hour_end": null,
  "total_duration_hour": null,
  "max_price": null,
  "sort_type": 2,
  "transfer_city": [],
  "min_transfer_minutes": 25,
  "max_transfer_minutes": 120,
  "frequent_routes": []
}


path: references/flyai-cli-reference.md
content: # flyai search-train 命令参考

## 参数速查

| 参数 | 说明 | 示例 |
|------|------|------|
| `--origin` | 出发城市/车站（必填） | `"北京"` |
| `--destination` | 目的地城市/车站 | `"上海"` |
| `--dep-date` | 出发日期 | `2026-04-03` |
| `--dep-date-start/end` | 出发日期范围 | `2026-04-03` ~ `2026-04-05` |
| `--back-date` | 回程日期 | `2026-04-10` |
| `--back-date-start/end` | 回程日期范围 | `2026-04-10` ~ `2026-04-12` |
| `--journey-type` | 1=直达, 2=中转 | `2` |
| `--seat-class-name` | 坐席，逗号分隔（二等座/一等座/商务座/硬卧/软卧） | `"硬卧,软卧"` |
| `--transport-no` | 车次号，逗号分隔 | `"G71,G73"` |
| `--transfer-city` | 中转城市，逗号分隔 | `"长沙,武汉"` |
| `--dep-hour-start/end` | 出发时段 0-24 | `12h` ~ `18h` |
| `--arr-hour-start/end` | 到达时段 0-24 | `6` ~ `22` |
| `--total-duration-hour` | 最大总时长（小时） | `8` |
| `--max-price` | 最高价格（元） | `800` |
| `--sort-type` | 1=价高→低, 2=推荐, 3=价低→高, 4=时短→长, 5=时长→短, 6=出发早, 7=出发晚, 8=直达优先 | `4` |

## 参数映射规则（重要）

用户明确提到的查询条件**必须**对应到相应参数：
- 用户说"硬卧" → `--seat-class-name "硬卧"`（⚠️ 中转查询不支持此参数，需 Step 2c 单独查询）
- 用户说"G71" → `--transport-no "G71"`
- 用户说“下午出发” → `--dep-hour-start 12h --dep-hour-end 18h`
- 用户说"8小时以内" → `--total-duration-hour 8`
- 用户说"不超过500块" → `--max-price 500`

## 返回数据结构

```json
{
  "data": {
    "itemList": [{
      "adultPrice": "¥553.0",
      "journeys": [{
        "segments": [{
          "depCityName": "北京", "depStationName": "北京南",
          "depDateTime": "2026-04-03 08:00:00",
          "arrCityName": "上海", "arrStationName": "上海虹桥",
          "arrDateTime": "2026-04-03 12:28:00",
          "duration": "268分钟",
          "marketingTransportNo": "G11",
          "seatClassName": "二等座"
        }],
        "totalDuration": "268分钟"
      }],
      "jumpUrl": "https://..."
    }]
  }
}
```

关键字段：`itemList` 方案列表、`segments[]` 每段行程、`jumpUrl` 预订链接（必须展示）、`totalDuration` 总时长


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)