Skill: ucasytoo/trip-packer (version 1.0.1)

--- name: trip-packer slug: trip-packer version: 1.0.0 description: | 帮助用户将旅行行程数据打包成独立的 HTML 地图网页。 引导用户完成行程规划、生成符合 Schema 的 JSON、调用 trip-packer CLI 构建产物，并在构建完成后将 HTML 结果呈现给用户。 metadata: {"openclaw":{"emoji":"🗺️","requires":{"bins":["node"]},"os":["linux","darwin","win32"]}} --- ## When to Use 当用户有以下意图时触发： - 想生成旅行地图、制作行程 HTML、使用 trip-packer - 想规划某城市行程并导出可分享的网页 - 提供了行程信息，希望打包成单一 HTML 文件 - 询问如何把自己的旅行数据变成地图页面 ## Core Rules 1. **先规划，后写数据**：若用户只给了模糊需求，先用 `planning-guide.md` 引导问答，收集目的地、天数、住宿、每日景点/餐厅、交通方式，再写 JSON。 2. **默认落盘到 skill data 目录**：将生成的 `{city}.json` 和 `{city}-map.html` 优先放在 `skills/trip-packer/data/` 下；若用户指定了其他目录，按用户要求存放。 3. **严格遵循 Schema**：写 JSON 时参考 `references/schema-reference.md` 中的字段类型与必填要求，避免使用未定义的 `type`（如 `"food"`）。 4. **Schema 校验必过**：写完后必须执行 `npx trip-packer validate -d <文件>`；出现错误时逐条解释并修正，校验通过后再构建。 5. **正确调用 CLI 构建**： - 单城市：`npx trip-packer build -d xxx.json -o xxx-map.html` - 多城市：多次 `-d`，可用 `--default-city` 指定默认城市 - 若需要同时生成分享图片，追加 `--images`，会在 HTML 同目录下自动生成 `xxx-map-panorama.png`（全景横图）和 `xxx-map-itinerary-vertical.png`（完整竖图） 6. **必须交付产物**：每次 `build` 成功后，向用户汇报产物文件路径、文件大小，并提示可直接用浏览器打开查看。若使用了 `--images`，需一并汇报两张 PNG 的路径和大小。 7. **不污染源码**：不要直接修改仓库 `src/data/` 下的硬编码数据文件（如 `seoul.json`），除非用户明确指令这么做。 8. **鼓励迭代**：先做一个最小可行版本（2 天、3-4 个地点），验证并构建出 HTML 让用户看到效果，再逐步扩展。 ## Quick Reference | 需求 | 文件 | |------|------| | 行程规划对话流程 | `planning-guide.md` | | 完整 JSON 字段说明 | `references/schema-reference.md` | | JSON 结构示例 | `references/sample-itinerary.json` |

path: planning-guide.md
content: # Trip Packer Planning Guide

Use this guide to help users plan a trip and produce valid trip-packer JSON.

## Planning Workflow

### Step 1: Gather Basics
- **Destination(s)**: City name and country (ISO code like `CN`, `JP`, `KR`).
- **Duration**: Number of days.
- **Hotel**: Name, address, and coordinates (or a well-known landmark near the city center).

### Step 2: Build Daily Routes
For each day, ask:
- **Theme/area** (e.g., "East Kyoto temples")
- **Spots**: 2–5 locations with names, addresses, and approximate coordinates.
- **Transport**: How they move between spots (walk, subway, taxi, train). Ask for rough duration/distance.
- **Meals** (optional): Restaurants can be added as `spot` locations with `parentId` pointing to the hotel or a nearby group.

### Step 3: Choose Map Tile Source
Set `metadata.country` based on the ISO country code:
- `"CN"` → uses Amap (Gaode) tiles for users in China.
- Anything else → uses CARTO CDN tiles.

## JSON Structure Cheat Sheet

### `metadata`
```json
{
  "title": "Kyoto 4-Day Trip",
  "subtitle": "3 nights near Kyoto Station",
  "mapCenter": { "lat": 35.01, "lng": 135.77 },
  "mapZoom": 13,
  "cityLabel": "京都",
  "seasonLabel": "AUTUMN",
  "flag": "🇯🇵",
  "country": "JP"
}
```

### `locations`
Two types:
- **`hotel_group` / `group`**: A parent location with `children: ["spot-id"]`, `lat`, `lng`, `color`.
- **`spot`**: A specific place with `parentId` pointing to its group/hotel, `lat`, `lng`, `color`, `type: "spot"`.

### `days`
Each day needs:
- `day`: 1-based number
- `title`, `note`, `baseHotelId`
- `path`: ordered array of `PathPoint`

#### `PathPoint`
```json
{
  "locationId": "spot-id",
  "label": "Walk · about 10 min",
  "isHotel": false,
  "transit": {
    "distance": "about 1 km",
    "duration": "about 10 min",
    "startName": "Spot A",
    "endName": "Spot B",
    "steps": [
      {
        "mode": "walk",
        "from": "Spot A",
        "to": "Spot B",
        "duration": "about 10 min",
        "distance": "about 1 km",
        "instruction": "Head north along the river"
      }
    ]
  }
}
```

Rules:
- `path[0]` should usually be the hotel (with `isHotel: true`).
- The last point can also be the hotel with `isHotel: true`.
- `mode` values: `walk`, `subway`, `bus`, `train`, `taxi`, `airport`.

## Common CLI Commands

```bash
# Validate data
npx trip-packer validate -d kyoto.json

# Build single-city HTML
npx trip-packer build -d kyoto.json -o kyoto-map.html

# Build multi-city HTML
npx trip-packer build -d kyoto.json -d tokyo.json --default-city kyoto -o japan-trip.html
```

## Tips

- Keep `label` human-readable; it appears on the map route.
- If you don’t have exact coordinates, use a map search or estimate from a well-known nearby point.
- When in doubt, create a minimal 2-day itinerary first, validate it, build it, show the user the result, then expand.


path: data/README.md
content: # trip-packer 数据目录

此目录用于存放 trip-packer Skill 生成的行程 JSON 文件和 HTML 产物。

Agent 在引导用户规划行程后，可以将生成的 `{city}.json` 和 `{city}-map.html` 放在这里。


path: references/sample-itinerary.json
content: {
  "metadata": {
    "title": "京都古刹二日游",
    "subtitle": "京都站附近1晚 · 2日慢游",
    "mapCenter": { "lat": 35.0116, "lng": 135.7681 },
    "mapZoom": 13,
    "cityLabel": "京都",
    "seasonLabel": "AUTUMN",
    "flag": "🇯🇵",
    "country": "JP"
  },
  "locations": {
    "kyoto_station_hotel": {
      "id": "kyoto_station_hotel",
      "name": "京都格兰比亚大酒店",
      "lat": 34.985,
      "lng": 135.758,
      "color": "#3b82f6",
      "type": "hotel_group",
      "description": "JR京都站上盖 · 交通便利",
      "address": "京都府京都市下京区烏丸通塩小路下ル東塩小路町901",
      "children": []
    },
    "kiyomizudera": {
      "id": "kiyomizudera",
      "name": "清水寺",
      "type": "spot",
      "lat": 34.9949,
      "lng": 135.785,
      "color": "#ef4444",
      "description": "世界遗产 · 清水舞台",
      "address": "京都府京都市东山区清水1-294",
      "parentId": "kyoto_station_hotel"
    },
    "sannenzaka": {
      "id": "sannenzaka",
      "name": "二年坂三年坂",
      "type": "spot",
      "lat": 34.9975,
      "lng": 135.781,
      "color": "#8b5cf6",
      "description": "京都风情石板路",
      "address": "京都府京都市东山区清水2-204",
      "parentId": "kyoto_station_hotel"
    },
    "fushimi_inari": {
      "id": "fushimi_inari",
      "name": "伏见稻荷大社",
      "type": "spot",
      "lat": 34.9671,
      "lng": 135.7727,
      "color": "#f97316",
      "description": "千本鸟居",
      "address": "京都府京都市伏见区深草薮之内町68",
      "parentId": "kyoto_station_hotel"
    }
  },
  "days": [
    {
      "day": 1,
      "date": "2026-11-15",
      "title": "东山古刹巡礼",
      "note": "上午清水寺参拜与二年坂三年坂漫步",
      "baseHotelId": "kyoto_station_hotel",
      "path": [
        {
          "locationId": "kyoto_station_hotel",
          "label": "起点",
          "isHotel": true
        },
        {
          "locationId": "kiyomizudera",
          "label": "公交206路 · 约20分钟",
          "transit": {
            "distance": "约4公里",
            "duration": "约20分钟",
            "startName": "京都站",
            "endName": "清水寺",
            "steps": [
              {
                "mode": "bus",
                "from": "京都站",
                "to": "五条坂站",
                "duration": "约15分钟",
                "distance": "约4公里",
                "instruction": "乘坐市营巴士206路至五条坂"
              },
              {
                "mode": "walk",
                "from": "五条坂站",
                "to": "清水寺",
                "duration": "约5分钟",
                "distance": "约300米",
                "instruction": "沿清水坂步行 uphill"
              }
            ]
          }
        },
        {
          "locationId": "sannenzaka",
          "label": "步行 · 约5分钟",
          "transit": {
            "distance": "约300米",
            "duration": "约5分钟",
            "startName": "清水寺",
            "endName": "二年坂三年坂",
            "steps": [
              {
                "mode": "walk",
                "from": "清水寺",
                "to": "二年坂三年坂",
                "duration": "约5分钟",
                "distance": "约300米",
                "instruction": "沿清水坂下坡步行"
              }
            ]
          }
        },
        {
          "locationId": "kyoto_station_hotel",
          "label": "公交 · 约20分钟",
          "isHotel": true,
          "transit": {
            "distance": "约4公里",
            "duration": "约20分钟",
            "startName": "二年坂三年坂",
            "endName": "京都站",
            "steps": [
              {
                "mode": "walk",
                "from": "二年坂三年坂",
                "to": "清水道站",
                "duration": "约5分钟",
                "distance": "约300米",
                "instruction": "步行至清水道巴士站"
              },
              {
                "mode": "bus",
                "from": "清水道站",
                "to": "京都站",
                "duration": "约15分钟",
                "distance": "约4公里",
                "instruction": "乘坐市营巴士206路返回京都站"
              }
            ]
          }
        }
      ]
    },
    {
      "day": 2,
      "date": "2026-11-16",
      "title": "伏见稻荷朝圣",
      "note": "上午穿越千本鸟居",
      "baseHotelId": "kyoto_station_hotel",
      "path": [
        {
          "locationId": "kyoto_station_hotel",
          "label": "起点",
          "isHotel": true
        },
        {
          "locationId": "fushimi_inari",
          "label": "JR奈良线 · 约5分钟",
          "transit": {
            "distance": "约2公里",
            "duration": "约5分钟",
            "startName": "京都站",
            "endName": "伏见稻荷大社",
            "steps": [
              {
                "mode": "train",
                "line": "JR奈良线",
                "from": "京都站",
                "to": "稻荷站",
                "duration": "约2分钟",
                "distance": "约2公里",
                "instruction": "乘坐JR奈良线至稻荷站"
              },
              {
                "mode": "walk",
                "from": "稻荷站",
                "to": "伏见稻荷大社",
                "duration": "约3分钟",
                "distance": "约200米",
                "instruction": "出站即达大社入口"
              }
            ]
          }
        },
        {
          "locationId": "kyoto_station_hotel",
          "label": "JR奈良线 · 约5分钟",
          "isHotel": true,
          "transit": {
            "distance": "约2公里",
            "duration": "约5分钟",
            "startName": "稻荷站",
            "endName": "京都站",
            "steps": [
              {
                "mode": "walk",
                "from": "伏见稻荷大社",
                "to": "稻荷站",
                "duration": "约3分钟",
                "distance": "约200米",
                "instruction": "步行返回稻荷站"
              },
              {
                "mode": "train",
                "line": "JR奈良线",
                "from": "稻荷站",
                "to": "京都站",
                "duration": "约2分钟",
                "distance": "约2公里",
                "instruction": "乘坐JR奈良线返回京都站"
              }
            ]
          }
        }
      ]
    }
  ]
}


path: references/schema-reference.md
content: # trip-packer JSON Schema 完整字段参考

## 根结构：ItineraryData

```json
{
  "metadata": { ... },
  "locations": { ... },
  "days": [ ... ]
}
```

---

## metadata

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `title` | `string` | 是 | 行程主标题 | `"京都古刹二日游"` |
| `subtitle` | `string` | 是 | 副标题 | `"京都站附近1晚 · 2日慢游"` |
| `startDate` | `string` | 否 | 开始日期（ISO格式） | `"2026-11-15"` |
| `endDate` | `string` | 否 | 结束日期 | `"2026-11-16"` |
| `mapCenter` | `object` | 否 | 地图默认中心 | `{ "lat": 35.01, "lng": 135.77 }` |
| `mapZoom` | `number` | 否 | 默认缩放级别（推荐 12-14） | `13` |
| `cityLabel` | `string` | 否 | 城市本地名称 | `"京都"` |
| `seasonLabel` | `string` | 否 | 季节标签 | `"AUTUMN"` |
| `flag` | `string` | 否 | Emoji 国旗 | `"🇯🇵"` |
| `country` | `string` | 否 | **ISO 国家代码**。`"CN"` 用高德瓦片，其他用 CARTO | `"JP"` |

---

## locations

`locations` 是一个以 `id` 为键的字典，每个值只能是以下两种类型之一：

### 类型 A：LocationGroup（商圈/酒店组）

用于 `type: "group"` 或 `"hotel_group"`。

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `id` | `string` | 是 | 唯一标识 | `"kyoto_station_hotel"` |
| `name` | `string` | 是 | 显示名称 | `"京都格兰比亚大酒店"` |
| `lat` | `number` | 是 | 纬度 | `34.985` |
| `lng` | `number` | 是 | 经度 | `135.758` |
| `color` | `string` | 是 | 标记颜色（hex） | `"#3b82f6"` |
| `type` | `string` | 是 | 固定为 `"group"` 或 `"hotel_group"` | `"hotel_group"` |
| `description` | `string` | 否 | 简短描述 | `"JR京都站上盖 · 交通便利"` |
| `address` | `string` | 否 | 详细地址 | `"京都府京都市下京区..."` |
| `children` | `string[]` | 是 | 子地点 `id` 列表 | `[]` |

### 类型 B：Location（具体地点）

用于 `type: "spot"`（景点、餐厅、店铺等）。

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `id` | `string` | 是 | 唯一标识 | `"kiyomizudera"` |
| `name` | `string` | 是 | 显示名称 | `"清水寺"` |
| `lat` | `number` | 是 | 纬度 | `34.9949` |
| `lng` | `number` | 是 | 经度 | `135.785` |
| `color` | `string` | 是 | 标记颜色（hex） | `"#ef4444"` |
| `type` | `string` | 是 | 固定为 `"spot"` | `"spot"` |
| `description` | `string` | 否 | 简短描述 | `"世界遗产 · 清水舞台"` |
| `parentId` | `string` | 是 | 所属 `LocationGroup` 的 `id` | `"kyoto_station_hotel"` |
| `address` | `string` | 否 | 详细地址 | `"京都府京都市东山区..."` |

> **注意**：Schema 不识别 `"food"`，餐厅也必须用 `"spot"`。

---

## days

`days` 是一个数组，每个元素代表一天的行程。

### DayPlan

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `day` | `number` | 是 | 第几天（从 1 开始） | `1` |
| `date` | `string` | 否 | 具体日期 | `"2026-11-15"` |
| `title` | `string` | 是 | 当日标题 | `"东山古刹巡礼"` |
| `note` | `string` | 是 | 当日说明/备忘 | `"上午清水寺参拜..."` |
| `baseHotelId` | `string` | 是 | 当日住宿酒店的 `id` | `"kyoto_station_hotel"` |
| `path` | `PathPoint[]` | 是 | 当日完整路径 | 见下文 |

### PathPoint

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `locationId` | `string` | 是 | 对应 `locations` 中的 `id` | `"kiyomizudera"` |
| `label` | `string` | 是 | 路径线上显示的文字 | `"公交206路 · 约20分钟"` |
| `transit` | `TransitDetail` | 否 | 从**上一个点**到本点的交通详情 | 见下文 |
| `isHotel` | `boolean` | 否 | 标记是否为酒店（首/尾常用） | `true` |
| `notes` | `NoteItem[]` | 否 | 本地点的备注列表 | 见下文 |

#### TransitDetail

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `distance` | `string` | 是 | 总距离 | `"约4公里"` |
| `duration` | `string` | 是 | 总时长 | `"约20分钟"` |
| `fare` | `string` | 否 | 票价 | `"230日元"` |
| `steps` | `TransitStep[]` | 是 | 分段详情 | 见下文 |
| `startName` | `string` | 是 | 起点名称 | `"京都站"` |
| `endName` | `string` | 是 | 终点名称 | `"清水寺"` |

#### TransitStep

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `mode` | `string` | 是 | 交通方式。可选值：`walk`, `subway`, `bus`, `train`, `taxi`, `airport` | `"bus"` |
| `line` | `string` | 否 | 线路名称 | `"市营巴士206路"` |
| `from` | `string` | 是 | 出发地 | `"京都站"` |
| `to` | `string` | 是 | 目的地 | `"五条坂站"` |
| `duration` | `string` | 是 | 本段时间 | `"约15分钟"` |
| `distance` | `string` | 否 | 本段距离 | `"约4公里"` |
| `instruction` | `string` | 是 | 导航说明 | `"乘坐市营巴士206路至五条坂"` |

#### NoteItem

| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| `category` | `string` | 是 | 分类。可选：`food`, `shopping`, `tips`, `other` | `"food"` |
| `content` | `string` | 是 | 内容 | `"推荐尝试抹茶冰淇淋"` |

---

## 常见规则与建议

### `path` 顺序
- `path[0]` 通常应该是 `baseHotelId` 对应的酒店，并设置 `isHotel: true`。
- 最后回到酒店时，最后一个 `PathPoint` 也建议设置 `isHotel: true`。
- 搬家日（换酒店）时，`path[0]` 可以是旧酒店，`path[1]` 为新酒店。

### `mode` 使用场景
- `walk`：步行
- `subway`：地铁
- `bus`：公交/巴士
- `train`：火车/JR/高铁
- `taxi`：出租车
- `airport`：机场专线/机场铁路

### `label` 写法建议
- 有交通时：`"公交206路 · 约20分钟"`、`"步行 · 约5分钟"`
- 无交通时（起点/酒店）：`"起点"`、`"入住"`

### `country` 与瓦片
- `"CN"`：国内使用**高德地图**瓦片
- 其他如 `"JP"`、`"KR"`、`"US"`：使用 **CARTO CDN** 瓦片

---

## 完整最小示例

参见 `sample-itinerary.json`（京都 2 日游示例）。


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