Skill: harperbot/surf-query (version 1.2.0)

--- name: 台灣衝浪浪點查詢 description: 查詢台灣全島衝浪浪點資訊，含即時潮汐、風況、颱風動態、日出日落，可附帶查詢附近停車場。支援 Telegram、LINE、iMessage。 --- # surf_query 透過 Telegram、LINE 或 iMessage 查詢台灣衝浪浪點，即時顯示潮汐、風向風速、颱風動態與日出時間。 ## 功能 - **浪點搜尋**：依名稱（東河、蜜月灣...）或地區（宜蘭、台東、墾丁...）搜尋 - **附近浪點**：傳送定位點，查詢 30km 內所有已知浪點 - **即時潮汐**：今日高低潮時間（CWA 中央氣象署） - **即時風況**：風速、風向，並判斷是否為離岸風（好浪） - **颱風動態**：顯示距台灣 2000km 內的活動颱風位置與強度 - **日出日落**：依浪點座標計算今日日出日沒時間 - **季節判斷**：自動標示現在是否為最佳浪季 - **一鍵導航**：Apple Maps + Google Maps 連結 - **串接停車查詢**：選用，需搭配 parking_query skill ## 使用情境 ``` 用戶：東河附近有什麼浪點？ 回覆： 🏄 東河 (Donghe) 📍 台東縣 東河鄉 🟡 難度：中階 浪型：河口浪 ✅ 現在（秋季）是好浪季節 🌅 日出 05:52 🌇 日沒 17:48 🌊 潮汐：🔽乾潮 02:14（-87cm） 🔼滿潮 08:28（59cm）... 💨 風況：偏北風 3級（4m/s） ✅ 離岸風，浪面整潔 ⛅ 天氣：晴時多雲 📝 台灣浪況最一致的浪點之一，東河溪口，冬季長浪穩定，礁石底 🍎 Apple Maps → https://maps.apple.com/?ll=23.0767,121.3424&q=東河 🗺 Google Maps → https://www.google.com/maps/search/?api=1&query=23.0767,121.3424 ``` 適用頻道：Telegram、LINE、iMessage（BlueBubbles）、及其他 OpenClaw 支援的頻道 ## 前置需求 ### 1. 申請 CWA API Key（免費，即時潮汐/風況用） 前往 [opendata.cwa.gov.tw](https://opendata.cwa.gov.tw) 註冊帳號，取得授權碼。 在 OpenClaw 的 `openclaw.json` 中加入： ```json { "env": { "vars": { "CWA_API_KEY": "CWA-你的授權碼" } } } ``` > 不設定也能使用，只是不會顯示即時潮汐與風況。 ### 2. 安裝 Python 套件 ```bash pip3 install requests ``` ## 安裝 ```bash git clone https://github.com/Harperbot/openclaw-surf-query.git \ ~/.openclaw/skills/surf_query openclaw restart ``` ## 資料來源 | 用途 | API | |---|---| | 浪點資料庫（30 個） | swelleye.com + outdoorfun.com.tw（手動整理） | | 即時潮汐 | CWA `F-A0021-001` | | 風況天氣 | CWA `F-D0047-091` | | 颱風動態 | CWA `W-C0034-005` | | 日出日落 | 天文公式計算 | ## 覆蓋範圍 北部（新北）、東北部（宜蘭）、東部（花蓮、台東）、南部（屏東、墾丁）、西部（高雄、台南、台中、苗栗）

path: README.md
content: # 🏄 surf_query

台灣衝浪浪點查詢 OpenClaw Skill，透過 Telegram、LINE 或 iMessage 搜尋浪點，即時顯示潮汐、風況、颱風動態與日出時間，並附一鍵導航連結。

## 功能示範

```
你：東河附近有什麼浪點？

助理：
🏄 東河 (Donghe)
   📍 台東縣 東河鄉
   🟡 難度：中階　浪型：河口浪
   ✅ 現在（秋季）是好浪季節
   🌅 日出 05:52　🌇 日沒 17:48
   🌊 潮汐：🔽乾潮 02:14（-87cm）　🔼滿潮 08:28（59cm）　🔽乾潮 14:06（-64cm）　🔼滿潮 20:18（80cm）
   💨 風況：偏北風 3級（4m/s）　✅ 離岸風，浪面整潔
   ⛅ 天氣：晴時多雲
   📝 台灣浪況最一致的浪點之一，東河溪口，冬季長浪穩定，礁石底
   🍎 Apple Maps → https://maps.apple.com/?ll=23.0767,121.3424&q=東河
   🗺 Google Maps → https://www.google.com/maps/search/?api=1&query=23.0767,121.3424
```

## 特色

- **30 個浪點資料庫**：北部、東北部（宜蘭）、東部（花蓮、台東）、南部（屏東、墾丁）、西部
- **多種查詢方式**：浪點名稱、地區名稱、GPS 座標（附近 30km 內）
- **即時潮汐**：CWA 中央氣象署今日高低潮時間（高精度，依浪點配對最近測站）
- **即時風況**：風速、風向，自動判斷離岸風（好浪）或向岸風（浪面雜亂）
- **颱風動態**：顯示距台灣 2000km 內的活動颱風，超出範圍自動隱藏
- **日出日落**：依浪點座標以天文公式精確計算
- **難度分級**：🟢 初學者 / 🟡 初中階・中階 / 🟠 中進階 / 🔴 進階
- **季節判斷**：自動標示現在是否為最佳浪季
- **一鍵導航**：Apple Maps + Google Maps 導航連結
- **串接停車查詢**：選用，需搭配 [parking_query](https://github.com/Harperbot/openclaw-parking-query) skill

## 前置需求

### 1. OpenClaw

請先安裝並設定 [OpenClaw](https://github.com/openclaw/openclaw)。

### 2. CWA API Key（免費）

前往 [opendata.cwa.gov.tw](https://opendata.cwa.gov.tw) 註冊帳號，取得授權碼。

在 `~/.openclaw/openclaw.json` 的 `env.vars` 加入：

```json
{
  "env": {
    "vars": {
      "CWA_API_KEY": "CWA-你的授權碼"
    }
  }
}
```

> 不設定也能使用，只是不會顯示即時潮汐、風況與颱風動態。

### 3. Python 套件

```bash
pip3 install requests
```

## 安裝

```bash
# 1. Clone 到 OpenClaw skills 目錄
git clone https://github.com/Harperbot/openclaw-surf-query.git \
  ~/.openclaw/skills/surf_query

# 2. 設定 CWA API Key（見上方說明）

# 3. 重啟 gateway
openclaw restart
```

或透過 ClawHub 安裝：

```bash
clawhub install openclaw-surf-query
```

## 直接執行（測試用）

```bash
# 依名稱搜尋
python3 ~/.openclaw/skills/surf_query/surf_query.py --query 東河

# 依地區搜尋
python3 ~/.openclaw/skills/surf_query/surf_query.py --query 宜蘭

# 依座標查詢附近浪點（30km 內）
python3 ~/.openclaw/skills/surf_query/surf_query.py --lat 24.87 --lon 121.83

# 浪點 + 附近停車場
python3 ~/.openclaw/skills/surf_query/surf_query.py --query 東河 --mode parking

# 列出所有浪點
python3 ~/.openclaw/skills/surf_query/surf_query.py --list

# 不查即時資料（純離線，速度快）
python3 ~/.openclaw/skills/surf_query/surf_query.py --query 金樽 --no-live
```

## 更新

```bash
bash ~/.openclaw/skills/surf_query/update.sh
```

或手動：

```bash
cd ~/.openclaw/skills/surf_query && git pull
```

## 浪點資料庫

| 地區 | 浪點 |
|---|---|
| 北部（新北） | 沙崙、白沙灣、中角灣、金山、翡翠灣、萬里 |
| 東北（宜蘭） | 福隆、大溪蜜月灣、外澳、烏石港北堤、烏石港南堤、梗枋、蘇澳無尾港 |
| 東部（花蓮） | 和仁礫灘、花蓮北濱、鹽寮漁港、磯崎 |
| 東部（台東） | 八仙洞、成功、東河、金樽、都蘭、台東市區 |
| 南部（屏東） | 九棚、佳樂水、南灣、杉板灣（小琉球） |
| 西部 | 旗津、漁光島、外埔漁港、竹南 |

## 資料來源

| 用途 | 來源 | 狀態 |
|---|---|---|
| 浪點資料庫 | swelleye.com + outdoorfun.com.tw | 手動整理，內建 JSON |
| 即時潮汐 | CWA opendata `F-A0021-001` | 需 CWA_API_KEY |
| 風況天氣預報 | CWA opendata `F-D0047-091` | 需 CWA_API_KEY |
| 颱風動態 | CWA opendata `W-C0034-005` | 需 CWA_API_KEY |
| 日出日落 | 天文公式計算 | 免費，不需 API |
| 停車場即時空位 | TDX parking_query skill | 選用 |

## 與 parking_query 串接

安裝 [parking_query](https://github.com/Harperbot/openclaw-parking-query) 後，加上 `--mode parking` 可同時查詢浪點附近停車場：

```bash
python3 surf_query.py --query 東河 --mode parking
```

```
🏄 東河 (Donghe)
   ...（浪點資訊）

🅿️ 附近停車：
🅿️ 附近 500 公尺 有空位的停車場（Taitung）

1. 東河停車場
   空位：28 個　距離：210 公尺
   🍎 Apple Maps → ...
   🗺 Google Maps → ...
```

---

## 進階整合（選用）

以下為可選的進階功能，適合想要主動通知的使用者自行串接。所有頻道（Telegram、LINE、iMessage）均透過 OpenClaw 的 channel bindings 設定路由，不需個別修改程式碼。

### 颱風警報推播

CWA `W-C0034-005` 提供即時颱風動態，可在 cron 排程中定期查詢，颱風進入指定距離時自動推播：

```python
import requests, os

KEY = os.environ["CWA_API_KEY"]
r = requests.get(
    "https://opendata.cwa.gov.tw/api/v1/rest/datastore/W-C0034-005",
    params={"Authorization": KEY},
    verify=False  # CWA 憑證缺 SKI，Python 3.12+ 需加此參數
)
data = r.json()
cyclones = data["records"]["TropicalCyclones"]["TropicalCyclone"]
for tc in cyclones:
    fixes = tc["AnalysisData"]["Fix"]
    last = fixes[-1] if isinstance(fixes, list) else fixes
    # 計算距台灣距離，決定是否發送通知
```

搭配 OpenClaw cron 每小時執行，有警報時發送至 **Telegram / iMessage / LINE**。

### 長浪警戒通知

> ⚠️ `W-C0051-001` 可能需要進階 CWA 帳號權限，免費帳號目前無法存取。

若您的帳號有權限，可加入每日維護排程：

```python
r = requests.get(
    "https://opendata.cwa.gov.tw/api/v1/rest/datastore/W-C0051-001",
    params={"Authorization": KEY},
    verify=False
)
```

### 每日浪報推播（Telegram / iMessage / LINE）

結合 surf_query 與 CWA API，透過 OpenClaw cron 每日定時推播至任意已串接頻道：

```
🌅 今日浪報（東台灣）

🏄 東河：偏北風 3 級，中潮 09:30
         現在漲潮中，預計 09:00 附近最佳
🏄 金樽：偏北風 3 級，大潮 09:20

⛅ 今日天氣：晴時多雲，氣溫 22-28°C
🌀 颱風動態：無（距台灣 2000km 以上）
🌅 今日日出：05:52
```

**設定方式**（以 Telegram 為例）：

1. 在 `~/.openclaw/cron/jobs.json` 加入排程
2. 排程腳本呼叫 `surf_query.py` 並取得輸出
3. 透過 OpenClaw 的 `sendMessage` 工具發送至目標頻道

iMessage（BlueBubbles）與 LINE 的設定方式相同，只需在 OpenClaw agent bindings 中切換頻道。

---

## License

MIT


path: requirements.txt
content: requests>=2.28.0


path: skill.yml
content: name: surf_query
description: 台灣衝浪浪點查詢，支援依名稱/地區搜尋浪點資訊、附近浪點（座標）、以及附帶停車場查詢
version: 1.0.0
author: Harperbot

triggers:
  - pattern: "衝浪"
    description: 查詢台灣衝浪浪點
  - pattern: "浪點"
    description: 查詢衝浪地點
  - pattern: "surf"
    description: Taiwan surf spot query (English)

tools:
  - name: query_surf_spot
    description: 依浪點名稱或地區查詢衝浪資訊（難度、浪型、最佳季節），可選擇附帶附近停車查詢
    script: ~/.openclaw/skills/surf_query/surf_query.py
    args:
      - name: query
        flag: --query
        description: 浪點名稱（中/英）或地區（宜蘭、花蓮、台東、北部、東部、南部等）
        required: false
      - name: mode
        flag: --mode
        description: "查詢模式：info（僅浪點資訊）/ parking（附停車查詢）/ all"
        required: false
        default: info

  - name: nearby_surf_spots
    description: 依 GPS 座標查詢附近衝浪浪點（30km 內），可選擇附帶停車查詢
    script: ~/.openclaw/skills/surf_query/surf_query.py
    args:
      - name: lat
        flag: --lat
        description: 緯度
        required: true
      - name: lon
        flag: --lon
        description: 經度
        required: true
      - name: mode
        flag: --mode
        description: "查詢模式：info / parking / all"
        required: false
        default: info

  - name: list_surf_spots
    description: 列出所有台灣衝浪浪點清單（依地區分組）
    script: ~/.openclaw/skills/surf_query/surf_query.py
    args:
      - name: list
        flag: --list
        description: 列出所有浪點
        is_flag: true

examples:
  - input: "東河附近有哪些衝浪點"
    tool: query_surf_spot
    args: {query: "東河"}

  - input: "宜蘭有哪些浪點"
    tool: query_surf_spot
    args: {query: "宜蘭"}

  - input: "台東有衝浪點嗎，順便查停車"
    tool: query_surf_spot
    args: {query: "台東", mode: "parking"}

  - input: "我在 25.033, 121.565 附近有浪嗎"
    tool: nearby_surf_spots
    args: {lat: 25.033, lon: 121.565}


path: surf_query.py
content: # version: 1.2.0
# Taiwan Surf Query Skill
# 查詢台灣衝浪浪點資訊、即時浪況（CWA 潮汐+風況）、附近停車場
# Usage: python3 surf_query.py --query "東河" [--mode info|surf|parking|all]

import argparse
import datetime
import json
import math
import os
import sys
import subprocess
from pathlib import Path

try:
    import requests
except ImportError:
    requests = None

SKILL_DIR = Path(__file__).parent
SPOTS_FILE = SKILL_DIR / "taiwan_surf_spots.json"
CWA_BASE = "https://opendata.cwa.gov.tw/api/v1/rest/datastore"

# ─── 距離計算 ────────────────────────────────────────────────────────────────

def haversine(lat1, lon1, lat2, lon2):
    R = 6371000
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)
    a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
    return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))

# ─── 浪點資料庫 ──────────────────────────────────────────────────────────────

def load_spots():
    with open(SPOTS_FILE, encoding="utf-8") as f:
        data = json.load(f)
    return data["spots"]

def find_spot(query: str, spots: list) -> list:
    query = query.strip().lower()
    results = []
    for s in spots:
        score = 0
        if query in s["name_zh"].lower():
            score += 10
        if query in s["name_en"].lower():
            score += 10
        if query in s.get("city", "").lower():
            score += 5
        if query in s.get("district", "").lower():
            score += 5
        if query in s.get("region", "").lower():
            score += 3
        if score > 0:
            results.append((score, s))
    results.sort(key=lambda x: -x[0])
    return [s for _, s in results[:5]]

def nearby_spots(lat: float, lon: float, spots: list, radius_m=30000, max_n=5) -> list:
    results = []
    for s in spots:
        d = haversine(lat, lon, s["lat"], s["lon"])
        if d <= radius_m:
            results.append((d, s))
    results.sort(key=lambda x: x[0])
    return [(d, s) for d, s in results[:max_n]]

# ─── CWA API ─────────────────────────────────────────────────────────────────

def get_cwa_key() -> str | None:
    return os.environ.get("CWA_API_KEY")

# ─── 日出日落（天文公式，不依賴 API）────────────────────────────────────────

def calc_sunrise_sunset(lat: float, lon: float, date: datetime.date | None = None) -> tuple[str, str]:
    """回傳 (日出時間, 日沒時間) 字串，格式 HH:MM（台灣時間 UTC+8）"""
    if date is None:
        date = datetime.date.today()
    import math as _m
    # 儒略日
    a = (14 - date.month) // 12
    y = date.year + 4800 - a
    m = date.month + 12 * a - 3
    jdn = date.day + (153*m+2)//5 + 365*y + y//4 - y//100 + y//400 - 32045
    n = jdn - 2451545 + 0.5 - lon/360
    # 太陽平均黃經
    L = (280.460 + 0.9856474 * n) % 360
    g = _m.radians((357.528 + 0.9856003 * n) % 360)
    lam = _m.radians(L + 1.915 * _m.sin(g) + 0.020 * _m.sin(2*g))
    # 赤緯
    eps = _m.radians(23.439 - 0.0000004 * n)
    dec = _m.asin(_m.sin(eps) * _m.sin(lam))
    # 時角
    cos_h = (_m.cos(_m.radians(90.833)) - _m.sin(_m.radians(lat)) * _m.sin(dec)) / \
            (_m.cos(_m.radians(lat)) * _m.cos(dec))
    if abs(cos_h) > 1:
        return ("--:--", "--:--")
    h = _m.degrees(_m.acos(cos_h))
    # 中天時間
    eot_min = -7.655 * _m.sin(g) + 9.873 * _m.sin(2*lam + 3.588) + 0.439 * _m.sin(4*lam - 6.714)
    transit_utc = 12 - lon/15 - eot_min/60
    rise_utc = transit_utc - h/15
    set_utc = transit_utc + h/15
    def fmt(t):
        t = (t + 8) % 24  # UTC+8
        h2, m2 = int(t), int((t % 1) * 60)
        return f"{h2:02d}:{m2:02d}"
    return (fmt(rise_utc), fmt(set_utc))

# ─── 颱風查詢 ─────────────────────────────────────────────────────────────────

def get_typhoon_info() -> str | None:
    """回傳目前活動颱風資訊。無颱風回 None；有颱風但距台灣遠且無警報也回 None"""
    data = cwa_get("W-C0034-005", {})
    if not data:
        return None
    try:
        cyclones = data["records"]["TropicalCyclones"]["TropicalCyclone"]
        alerts = []
        for tc in cyclones:
            name = tc.get("TyphoonName", "")
            name_zh = tc.get("CwaTyphoonName", "") or name
            ty_no = tc.get("CwaTyNo", "")    # 有值 = CWA 已命名颱風
            td_no = tc.get("CwaTdNo", "")    # 有值 = 熱帶低壓

            # 取最新位置
            fixes = tc.get("AnalysisData", {}).get("Fix", [])
            if isinstance(fixes, dict):
                fixes = [fixes]
            if not fixes:
                continue
            last = fixes[-1]
            lat = float(last.get("CoordinateLatitude", 0))
            lon = float(last.get("CoordinateLongitude", 0))
            max_wind = last.get("MaxWindSpeed", "")
            direction = last.get("MovingDirection", "")
            speed = last.get("MovingSpeed", "")

            # 與台灣的距離（台灣中心 23.5N, 121E）
            dist_km = haversine(lat, lon, 23.5, 121.0) / 1000

            # 強度標籤
            if ty_no:
                intensity = "颱風"
                emoji = "🌀"
            else:
                intensity = "熱帶低壓"
                emoji = "🌬️"

            label = f"{intensity} {name_zh}"
            if dist_km < 2000:
                alerts.append(
                    f"   {emoji} {label}｜距台灣 {dist_km:.0f}km｜"
                    f"最大風速 {max_wind}m/s｜往{direction}移動 {speed}km/h"
                )
            # 超過 2000km 且無警報不顯示

        return "\n".join(alerts) if alerts else None
    except Exception:
        return None

def cwa_get(dataset: str, params: dict) -> dict | None:
    if requests is None:
        return None
    key = get_cwa_key()
    if not key:
        return None
    try:
        params["Authorization"] = key
        # verify=False: CWA 憑證缺 Subject Key Identifier，Python 3.14 拒絕
        import urllib3
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        r = requests.get(f"{CWA_BASE}/{dataset}", params=params, timeout=8, verify=False)
        d = r.json()
        return d if d.get("success") == "true" else None
    except Exception:
        return None

def get_tide_today(station_id: str) -> str | None:
    """查今日潮汐高低潮時間"""
    data = cwa_get("F-A0021-001", {"LocationId": station_id})
    if not data:
        return None
    try:
        forecasts = data["records"]["TideForecasts"]
        loc = forecasts[0]["Location"]
        today_str = datetime.date.today().isoformat()
        # 找今天的資料
        for day in loc["TimePeriods"]["Daily"]:
            if day["Date"] == today_str:
                parts = []
                for t in day["Time"]:
                    tide_type = "🔼滿潮" if t["Tide"] == "滿潮" else "🔽乾潮"
                    time_str = t["DateTime"][11:16]
                    height = t["TideHeights"]["AboveLocalMSL"]
                    parts.append(f"{tide_type} {time_str}（{height}cm）")
                return "　".join(parts)
        # 如果今天沒資料，用第一筆
        day = loc["TimePeriods"]["Daily"][0]
        parts = []
        for t in day["Time"]:
            tide_type = "🔼滿潮" if t["Tide"] == "滿潮" else "🔽乾潮"
            time_str = t["DateTime"][11:16]
            height = t["TideHeights"]["AboveLocalMSL"]
            parts.append(f"{tide_type} {time_str}（{height}cm）")
        return "　".join(parts) + f"（{day['Date']}）"
    except Exception:
        return None

def get_current_tide_phase(station_id: str) -> str:
    """判斷現在是漲潮還是退潮中"""
    data = cwa_get("F-A0021-001", {"LocationId": station_id})
    if not data:
        return ""
    try:
        forecasts = data["records"]["TideForecasts"]
        loc = forecasts[0]["Location"]
        now = datetime.datetime.now(tz=datetime.timezone(datetime.timedelta(hours=8)))
        all_times = []
        for day in loc["TimePeriods"]["Daily"][:2]:
            for t in day["Time"]:
                dt = datetime.datetime.fromisoformat(t["DateTime"])
                all_times.append((dt, t["Tide"], t["TideHeights"]["AboveLocalMSL"]))
        all_times.sort(key=lambda x: x[0])
        # 找最近的前後潮
        prev, nxt = None, None
        for i, (dt, tide, h) in enumerate(all_times):
            if dt > now:
                nxt = (dt, tide, h)
                if i > 0:
                    prev = all_times[i-1]
                break
        if prev and nxt:
            if nxt[1] == "滿潮":
                return f"↑ 漲潮中（{nxt[0].strftime('%H:%M')} 滿潮）"
            else:
                return f"↓ 退潮中（{nxt[0].strftime('%H:%M')} 乾潮）"
    except Exception:
        pass
    return ""

def get_wind_weather(county: str) -> dict | None:
    """查縣市風速/風向/天氣"""
    data = cwa_get("F-D0047-091", {"elementName": "WindSpeed,WindDirection,Weather"})
    if not data:
        return None
    try:
        locs = data["records"]["Locations"]
        for lg in locs:
            for loc in lg["Location"]:
                if county in loc["LocationName"] or loc["LocationName"] in county:
                    result = {}
                    for el in loc["WeatherElement"]:
                        t0 = el["Time"][0]
                        val = t0.get("ElementValue", [])
                        name = el["ElementName"]
                        if name == "風速" and val:
                            result["wind_speed"] = val[0].get("WindSpeed", "")
                            result["beaufort"] = val[0].get("BeaufortScale", "")
                        elif name == "風向" and val:
                            result["wind_dir"] = val[0].get("WindDirection", "")
                        elif name == "天氣現象" and val:
                            result["weather"] = val[0].get("Weather", "")
                    return result if result else None
    except Exception:
        pass
    return None

def wind_surf_quality(wind_dir: str, best_wind: str) -> str:
    """判斷風向對浪況是否有利"""
    offshore_map = {
        "west": ["偏西風", "西北風", "西南風"],
        "southwest": ["西南風", "偏西風", "偏南風"],
        "south": ["偏南風", "西南風", "東南風"],
        "east": ["偏東風", "東南風", "東北風"],
        "north": ["偏北風", "東北風", "西北風"],
    }
    good_winds = offshore_map.get(best_wind, [])
    for w in good_winds:
        if w in wind_dir:
            return "✅ 離岸風，浪面整潔"
    onshore = {
        "west": ["偏東風"], "southwest": ["東北風"], "south": ["偏北風"],
        "east": ["偏西風"], "north": ["偏南風"],
    }
    bad_winds = onshore.get(best_wind, [])
    for w in bad_winds:
        if w in wind_dir:
            return "⚠️ 向岸風，浪面雜亂"
    return "➡️ 側風"

# ─── 季節判斷 ─────────────────────────────────────────────────────────────────

def get_season_note(spot: dict) -> str:
    month = datetime.datetime.now().month
    seasons = {"spring": [3,4,5], "summer": [6,7,8], "autumn": [9,10,11], "winter": [12,1,2]}
    season_zh = {"spring": "春季", "summer": "夏季", "autumn": "秋季", "winter": "冬季"}
    current = next((s for s, months in seasons.items() if month in months), "unknown")
    best = spot.get("best_season", [])
    best_zh = [season_zh.get(s, s) for s in best]
    if current in best:
        return f"✅ 現在（{season_zh[current]}）是好浪季節"
    return f"⚠️ 現在（{season_zh[current]}）非主要浪季（最佳：{'、'.join(best_zh)}）"

# ─── 停車查詢 ─────────────────────────────────────────────────────────────────

def find_nearby_parking(lat: float, lon: float) -> str:
    parking_script = Path.home() / ".openclaw/skills/parking_query/parking_query.py"
    venv_python = Path.home() / ".openclaw/venv/bin/python3"
    python = str(venv_python) if venv_python.exists() else sys.executable
    if not parking_script.exists():
        return "（停車查詢 skill 未安裝）"
    try:
        result = subprocess.run(
            [python, str(parking_script), "--lat", str(lat), "--lon", str(lon), "--mode", "realtime"],
            capture_output=True, text=True, timeout=30
        )
        return result.stdout.strip() or "（無停車資料）"
    except Exception as e:
        return f"（停車查詢失敗：{e}）"

# ─── 格式化輸出 ───────────────────────────────────────────────────────────────

DIFFICULTY_EMOJI = {
    "beginner": "🟢", "beginner_intermediate": "🟡", "intermediate": "🟡",
    "intermediate_advanced": "🟠", "advanced": "🔴",
}
DIFFICULTY_ZH = {
    "beginner": "初學者", "beginner_intermediate": "初中階", "intermediate": "中階",
    "intermediate_advanced": "中進階", "advanced": "進階",
}
WAVE_ZH = {"beach_break": "沙灘浪", "point_break": "礁石浪", "river_mouth": "河口浪"}

def format_spot_info(spot: dict, distance_m: float | None = None,
                     include_parking: bool = False, include_live: bool = True) -> str:
    diff_emoji = DIFFICULTY_EMOJI.get(spot.get("difficulty", ""), "⚪")
    diff_text = DIFFICULTY_ZH.get(spot.get("difficulty", ""), spot.get("difficulty", ""))
    wave_text = WAVE_ZH.get(spot.get("wave_type", ""), spot.get("wave_type", ""))

    dist_text = f"　距離：{distance_m/1000:.1f} km" if distance_m is not None else ""
    apple_maps = f"https://maps.apple.com/?ll={spot['lat']},{spot['lon']}&q={spot['name_zh']}"
    google_maps = f"https://www.google.com/maps/search/?api=1&query={spot['lat']},{spot['lon']}"

    sunrise, sunset = calc_sunrise_sunset(spot["lat"], spot["lon"])

    lines = [
        f"🏄 {spot['name_zh']} ({spot['name_en']}){dist_text}",
        f"   📍 {spot['city']} {spot['district']}",
        f"   {diff_emoji} 難度：{diff_text}　浪型：{wave_text}",
        f"   {get_season_note(spot)}",
        f"   🌅 日出 {sunrise}　🌇 日沒 {sunset}",
    ]

    # CWA 即時資料
    if include_live and get_cwa_key():
        # 潮汐
        station = spot.get("cwa_tide_station")
        if station:
            tide_today = get_tide_today(station)
            tide_phase = get_current_tide_phase(station)
            if tide_today:
                lines.append(f"   🌊 潮汐：{tide_today}")
            if tide_phase:
                lines.append(f"        現在：{tide_phase}")

        # 颱風
        typhoon = get_typhoon_info()
        if typhoon:
            lines.append(f"   {typhoon}")

        # 風況
        county = spot.get("cwa_county")
        if county:
            wind = get_wind_weather(county)
            if wind:
                wind_note = ""
                if wind.get("wind_dir") and spot.get("best_wind"):
                    wind_note = f"　{wind_surf_quality(wind['wind_dir'], spot['best_wind'])}"
                beaufort = wind.get("beaufort", "")
                speed = wind.get("wind_speed", "")
                weather = wind.get("weather", "")
                lines.append(
                    f"   💨 風況：{wind.get('wind_dir','未知')} {beaufort}級（{speed}m/s）{wind_note}"
                )
                if weather:
                    lines.append(f"   ⛅ 天氣：{weather}")

    lines += [
        f"   📝 {spot.get('notes', '')}",
        f"   🍎 Apple Maps → {apple_maps}",
        f"   🗺 Google Maps → {google_maps}",
    ]

    if include_parking:
        lines.append("\n🅿️ 附近停車：")
        lines.append(find_nearby_parking(spot["lat"], spot["lon"]))

    return "\n".join(lines)

# ─── 主程式 ───────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="台灣衝浪浪點查詢")
    parser.add_argument("--query", help="搜尋浪點名稱（中/英）或地區")
    parser.add_argument("--lat", type=float, help="緯度（查詢附近浪點）")
    parser.add_argument("--lon", type=float, help="經度（查詢附近浪點）")
    parser.add_argument("--region", help="地區篩選：north/northeast/east/south/west")
    parser.add_argument("--mode", default="info",
                        choices=["info", "parking", "all"],
                        help="info=浪點資訊, parking=附帶停車查詢, all=全部")
    parser.add_argument("--no-live", action="store_true", help="不查 CWA 即時資料（純離線）")
    parser.add_argument("--list", action="store_true", help="列出所有浪點")
    args = parser.parse_args()

    spots = load_spots()
    include_parking = args.mode in ("parking", "all")
    include_live = not args.no_live

    if not get_cwa_key() and include_live:
        print("💡 提示：設定 CWA_API_KEY 環境變數可顯示即時潮汐與風況\n")

    # 列出所有浪點
    if args.list:
        region_zh = {"north": "北部", "northeast": "東北部", "east": "東部",
                     "south": "南部", "west": "西部"}
        by_region = {}
        for s in spots:
            r = s.get("region", "other")
            by_region.setdefault(r, []).append(s)
        for r, r_spots in by_region.items():
            print(f"\n【{region_zh.get(r, r)}】")
            for s in r_spots:
                diff = DIFFICULTY_EMOJI.get(s.get("difficulty", ""), "⚪")
                print(f"  {diff} {s['name_zh']} ({s['name_en']}) - {s['city']}{s['district']}")
        return

    # 依座標查詢附近浪點
    if args.lat and args.lon:
        nearby = nearby_spots(args.lat, args.lon, spots)
        if not nearby:
            print("⚠️ 附近 30km 內沒有已知衝浪浪點")
            return
        print(f"🏄 附近衝浪浪點（{args.lat:.4f}, {args.lon:.4f}）\n")
        for i, (dist, s) in enumerate(nearby, 1):
            if args.region and s.get("region") != args.region:
                continue
            print(f"{i}. {format_spot_info(s, dist, include_parking, include_live)}\n")
        return

    # 依名稱/地區搜尋
    if args.query:
        region_map = {
            "北部": "north", "北台灣": "north", "新北": "north", "台北": "north",
            "東北": "northeast", "宜蘭": "northeast",
            "東部": "east", "東台灣": "east", "花蓮": "east", "台東": "east",
            "南部": "south", "南台灣": "south", "屏東": "south", "墾丁": "south",
            "西部": "west", "西台灣": "west", "台中": "west", "台南": "west", "高雄": "west",
        }
        region_key = next((v for k, v in region_map.items() if k in args.query), None)

        if region_key:
            filtered = [s for s in spots if s.get("region") == region_key]
            if not filtered:
                print(f"⚠️ 找不到「{args.query}」的浪點")
                return
            region_zh = {"north": "北部", "northeast": "東北部", "east": "東部",
                         "south": "南部", "west": "西部"}
            print(f"🏄 {region_zh.get(region_key, args.query)} 衝浪浪點（共 {len(filtered)} 個）\n")
            for i, s in enumerate(filtered, 1):
                print(f"{i}. {format_spot_info(s, None, include_parking, include_live)}\n")
        else:
            found = find_spot(args.query, spots)
            if not found:
                print(f"⚠️ 找不到「{args.query}」的浪點\n")
                print("💡 試試：--list（列出所有）｜--query 宜蘭｜--query east")
                return
            print(f"🏄 搜尋結果：「{args.query}」\n")
            for i, s in enumerate(found, 1):
                print(f"{i}. {format_spot_info(s, None, include_parking, include_live)}\n")
        return

    parser.print_help()

if __name__ == "__main__":
    main()


path: taiwan_surf_spots.json
content: {
  "version": "1.1.0",
  "updated": "2026-03-04",
  "source": "swelleye.com + outdoorfun.com.tw + CWA opendata",
  "notes": "座標為近似值（精度約 100-200m）。cwa_tide_station: CWA F-A0021-001 LocationId。cwa_county: CWA F-D0047-091 縣市名稱（臺灣正體）",
  "spots": [
    {
      "id": "shalun",
      "name_zh": "沙崙",
      "name_en": "Shalun",
      "region": "north",
      "city": "新北市",
      "district": "淡水區",
      "lat": 25.1758,
      "lon": 121.4098,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["autumn", "winter"],
      "best_wind": "south",
      "cwa_county": "新北市",
      "cwa_tide_station": "65000100",
      "notes": "淡水河口南側沙灘，新手友善"
    },
    {
      "id": "baishawan",
      "name_zh": "白沙灣",
      "name_en": "Baishawan",
      "region": "north",
      "city": "新北市",
      "district": "三芝區",
      "lat": 25.2764,
      "lon": 121.5378,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["autumn", "winter"],
      "best_wind": "south",
      "cwa_county": "新北市",
      "cwa_tide_station": "65000210",
      "notes": "白色沙灘，適合初學者，北海岸熱門景點"
    },
    {
      "id": "zhongjiaobay",
      "name_zh": "中角灣",
      "name_en": "Zhongjiao Bay",
      "region": "north",
      "city": "新北市",
      "district": "金山區",
      "lat": 25.2362,
      "lon": 121.6066,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "southwest",
      "cwa_county": "新北市",
      "cwa_tide_station": "O00100",
      "notes": "國際衝浪基地，設施完善，WSL 賽事曾舉辦地"
    },
    {
      "id": "jinshan",
      "name_zh": "金山",
      "name_en": "Jinshan",
      "region": "north",
      "city": "新北市",
      "district": "金山區",
      "lat": 25.2280,
      "lon": 121.6380,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "south",
      "cwa_county": "新北市",
      "cwa_tide_station": "65000270",
      "notes": "金山老街附近，交通便利"
    },
    {
      "id": "greenbay",
      "name_zh": "翡翠灣",
      "name_en": "Green Bay",
      "region": "north",
      "city": "新北市",
      "district": "萬里區",
      "lat": 25.2332,
      "lon": 121.6170,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["autumn", "winter"],
      "best_wind": "south",
      "cwa_county": "新北市",
      "cwa_tide_station": "A01500",
      "notes": "翡翠灣渡假區，適合家庭"
    },
    {
      "id": "wanli",
      "name_zh": "萬里",
      "name_en": "Wanli",
      "region": "north",
      "city": "新北市",
      "district": "萬里區",
      "lat": 25.1920,
      "lon": 121.7034,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "south",
      "cwa_county": "新北市",
      "cwa_tide_station": "65000280",
      "notes": "野柳附近，北海岸衝浪熱點"
    },
    {
      "id": "fulong",
      "name_zh": "福隆",
      "name_en": "Fulong",
      "region": "northeast",
      "city": "新北市",
      "district": "貢寮區",
      "lat": 25.0239,
      "lon": 121.9435,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter", "spring"],
      "best_wind": "west",
      "cwa_county": "新北市",
      "cwa_tide_station": "O00300",
      "notes": "雙溪河口沙洲，左右浪型，著名沙灘，注意水流"
    },
    {
      "id": "daxi_honeymoon",
      "name_zh": "大溪蜜月灣",
      "name_en": "Daxi Honeymoon Bay",
      "region": "northeast",
      "city": "宜蘭縣",
      "district": "頭城鎮",
      "lat": 24.8654,
      "lon": 121.8512,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "west",
      "cwa_county": "宜蘭縣",
      "cwa_tide_station": "10002040",
      "notes": "宜蘭知名浪點，台灣十大衝浪點之一，東北季風期浪況穩定"
    },
    {
      "id": "waiao",
      "name_zh": "外澳",
      "name_en": "Wai'ao",
      "region": "northeast",
      "city": "宜蘭縣",
      "district": "頭城鎮",
      "lat": 24.9028,
      "lon": 121.8220,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "west",
      "cwa_county": "宜蘭縣",
      "cwa_tide_station": "I02200",
      "notes": "外澳火車站對面，交通便利，沙礫混合底"
    },
    {
      "id": "wushi_north",
      "name_zh": "烏石港北堤",
      "name_en": "Wushi North Breakwater",
      "region": "northeast",
      "city": "宜蘭縣",
      "district": "頭城鎮",
      "lat": 24.8735,
      "lon": 121.8308,
      "wave_type": "point_break",
      "difficulty": "intermediate_advanced",
      "best_season": ["autumn", "winter"],
      "best_wind": "southwest",
      "cwa_county": "宜蘭縣",
      "cwa_tide_station": "O00400",
      "notes": "北堤旁，浪型較為穩定，中進階玩家"
    },
    {
      "id": "wushi_south",
      "name_zh": "烏石港南堤",
      "name_en": "Wushi South Breakwater",
      "region": "northeast",
      "city": "宜蘭縣",
      "district": "頭城鎮",
      "lat": 24.8705,
      "lon": 121.8285,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "west",
      "cwa_county": "宜蘭縣",
      "cwa_tide_station": "O00400",
      "notes": "南堤旁，較北堤和緩，初中階皆宜"
    },
    {
      "id": "gengfang",
      "name_zh": "梗枋",
      "name_en": "Gengfang",
      "region": "northeast",
      "city": "宜蘭縣",
      "district": "頭城鎮",
      "lat": 24.8168,
      "lon": 121.8300,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "west",
      "cwa_county": "宜蘭縣",
      "cwa_tide_station": "I02200",
      "notes": "頭城南側，宜蘭十大衝浪點之一"
    },
    {
      "id": "suao_wuwei",
      "name_zh": "蘇澳無尾港",
      "name_en": "Suao Wuwei",
      "region": "northeast",
      "city": "宜蘭縣",
      "district": "蘇澳鎮",
      "lat": 24.5987,
      "lon": 121.8550,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "west",
      "cwa_county": "宜蘭縣",
      "cwa_tide_station": "10002030",
      "notes": "蘇澳南方，靠近無尾港水鳥保護區"
    },
    {
      "id": "hualien_beibin",
      "name_zh": "花蓮北濱",
      "name_en": "Hualien North Coast",
      "region": "east",
      "city": "花蓮縣",
      "district": "花蓮市",
      "lat": 23.9998,
      "lon": 121.6191,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter", "summer"],
      "best_wind": "west",
      "cwa_county": "花蓮縣",
      "cwa_tide_station": "10015010",
      "notes": "花蓮市北濱公園旁，交通便利，全年可浪"
    },
    {
      "id": "yanliao",
      "name_zh": "鹽寮漁港",
      "name_en": "Yanliao Fishing Harbor",
      "region": "east",
      "city": "花蓮縣",
      "district": "壽豐鄉",
      "lat": 23.9378,
      "lon": 121.6392,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter", "summer"],
      "best_wind": "west",
      "cwa_county": "花蓮縣",
      "cwa_tide_station": "10015060",
      "notes": "花蓮七星潭南方，Gongs 浪點"
    },
    {
      "id": "jiqi",
      "name_zh": "磯崎",
      "name_en": "Jiqi",
      "region": "east",
      "city": "花蓮縣",
      "district": "豐濱鄉",
      "lat": 23.7985,
      "lon": 121.6042,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter", "summer"],
      "best_wind": "west",
      "cwa_county": "花蓮縣",
      "cwa_tide_station": "10015080",
      "notes": "花蓮南部海岸，較少人、自然環境佳"
    },
    {
      "id": "heren",
      "name_zh": "和仁礫灘",
      "name_en": "Heren Gravel Beach",
      "region": "east",
      "city": "花蓮縣",
      "district": "秀林鄉",
      "lat": 24.2200,
      "lon": 121.6020,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["autumn", "winter", "summer"],
      "best_wind": "west",
      "cwa_county": "花蓮縣",
      "cwa_tide_station": "10015110",
      "notes": "花蓮北部礫石灘，景色壯麗，鄰近清水斷崖"
    },
    {
      "id": "baxiandong",
      "name_zh": "八仙洞",
      "name_en": "Eight Immortals Cave",
      "region": "east",
      "city": "台東縣",
      "district": "長濱鄉",
      "lat": 23.4490,
      "lon": 121.4626,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "southwest",
      "cwa_county": "臺東縣",
      "cwa_tide_station": "10014080",
      "notes": "台東長濱北端，知名衝浪點，礫石底"
    },
    {
      "id": "chenggong",
      "name_zh": "成功",
      "name_en": "Chenggong",
      "region": "east",
      "city": "台東縣",
      "district": "成功鎮",
      "lat": 23.1003,
      "lon": 121.3682,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter", "summer"],
      "best_wind": "west",
      "cwa_county": "臺東縣",
      "cwa_tide_station": "10014020",
      "notes": "成功鎮漁港附近，台東東海岸中段"
    },
    {
      "id": "donghe",
      "name_zh": "東河",
      "name_en": "Donghe",
      "region": "east",
      "city": "台東縣",
      "district": "東河鄉",
      "lat": 23.0767,
      "lon": 121.3424,
      "wave_type": "river_mouth",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "southwest",
      "cwa_county": "臺東縣",
      "cwa_tide_station": "10014070",
      "notes": "台灣浪況最一致的浪點之一，東河溪口，冬季長浪穩定，礁石底"
    },
    {
      "id": "jinzun",
      "name_zh": "金樽",
      "name_en": "Jinzun",
      "region": "east",
      "city": "台東縣",
      "district": "東河鄉",
      "lat": 23.0197,
      "lon": 121.3188,
      "wave_type": "beach_break",
      "difficulty": "intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "southwest",
      "cwa_county": "臺東縣",
      "cwa_tide_station": "O01300",
      "notes": "金樽漁港旁，台東知名浪點，景色優美"
    },
    {
      "id": "dulan",
      "name_zh": "都蘭",
      "name_en": "Dulan",
      "region": "east",
      "city": "台東縣",
      "district": "東河鄉",
      "lat": 22.9547,
      "lon": 121.2722,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter", "summer"],
      "best_wind": "west",
      "cwa_county": "臺東縣",
      "cwa_tide_station": "10014070",
      "notes": "都蘭灣，藝文衝浪聚落，全年可浪"
    },
    {
      "id": "taitung_city",
      "name_zh": "台東市區",
      "name_en": "Taitung City",
      "region": "east",
      "city": "台東縣",
      "district": "台東市",
      "lat": 22.7490,
      "lon": 121.1640,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["summer", "autumn"],
      "best_wind": "west",
      "cwa_county": "臺東縣",
      "cwa_tide_station": "10014010",
      "notes": "台東活水湖附近，WSL 台灣國際衝浪賽場地"
    },
    {
      "id": "jiupeng",
      "name_zh": "九棚",
      "name_en": "Jiupeng",
      "region": "south",
      "city": "屏東縣",
      "district": "滿州鄉",
      "lat": 22.2523,
      "lon": 120.8834,
      "wave_type": "beach_break",
      "difficulty": "intermediate_advanced",
      "best_season": ["summer", "autumn"],
      "best_wind": "north",
      "cwa_county": "屏東縣",
      "cwa_tide_station": "10013330",
      "notes": "屏東最南端，颱風浪強，較少遊客，沙丘地形"
    },
    {
      "id": "jialeshui",
      "name_zh": "佳樂水",
      "name_en": "Jialeshui",
      "region": "south",
      "city": "屏東縣",
      "district": "滿州鄉",
      "lat": 21.9573,
      "lon": 120.8527,
      "wave_type": "point_break",
      "difficulty": "intermediate_advanced",
      "best_season": ["summer", "autumn"],
      "best_wind": "north",
      "cwa_county": "屏東縣",
      "cwa_tide_station": "10013240",
      "notes": "墾丁東側，夏季颱風浪著名，礁石地形"
    },
    {
      "id": "nanwan",
      "name_zh": "南灣",
      "name_en": "Nanwan",
      "region": "south",
      "city": "屏東縣",
      "district": "恆春鎮",
      "lat": 21.9477,
      "lon": 120.8262,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["summer"],
      "best_wind": "north",
      "cwa_county": "屏東縣",
      "cwa_tide_station": "O00700",
      "notes": "墾丁南灣，觀光客多，夏季西南風浪，適合初學"
    },
    {
      "id": "shanban",
      "name_zh": "杉板灣",
      "name_en": "Shanban Bay",
      "region": "south",
      "city": "屏東縣",
      "district": "琉球鄉",
      "lat": 22.3330,
      "lon": 120.3760,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["summer"],
      "best_wind": "north",
      "cwa_county": "屏東縣",
      "cwa_tide_station": "10013220",
      "notes": "小琉球，適合初學，沙灘平緩，夏季南風浪"
    },
    {
      "id": "qijin",
      "name_zh": "旗津",
      "name_en": "Qijin",
      "region": "west",
      "city": "高雄市",
      "district": "旗津區",
      "lat": 22.5990,
      "lon": 120.2780,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["autumn", "winter"],
      "best_wind": "east",
      "cwa_county": "高雄市",
      "cwa_tide_station": "64000100",
      "notes": "高雄旗津沙灘，北風時有浪，西海岸難得衝浪點"
    },
    {
      "id": "yuguangdao",
      "name_zh": "漁光島",
      "name_en": "Yuguang Island",
      "region": "west",
      "city": "台南市",
      "district": "安平區",
      "lat": 22.9600,
      "lon": 120.1600,
      "wave_type": "beach_break",
      "difficulty": "beginner",
      "best_season": ["autumn", "winter"],
      "best_wind": "east",
      "cwa_county": "臺南市",
      "cwa_tide_station": "I00500",
      "notes": "台南安平，西海岸冬季北風浪"
    },
    {
      "id": "waipu",
      "name_zh": "外埔漁港",
      "name_en": "Waipu Fishing Harbor",
      "region": "west",
      "city": "台中市",
      "district": "大甲區",
      "lat": 24.2742,
      "lon": 120.5478,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "east",
      "cwa_county": "臺中市",
      "cwa_tide_station": "66000110",
      "notes": "台中大甲，西海岸冬季東北季風浪"
    },
    {
      "id": "zhunan",
      "name_zh": "竹南",
      "name_en": "Zhunan",
      "region": "west",
      "city": "苗栗縣",
      "district": "竹南鎮",
      "lat": 24.6758,
      "lon": 120.8657,
      "wave_type": "beach_break",
      "difficulty": "beginner_intermediate",
      "best_season": ["autumn", "winter"],
      "best_wind": "east",
      "cwa_county": "苗栗縣",
      "cwa_tide_station": "10005040",
      "notes": "苗栗西海岸，冬季北風時可浪"
    }
  ]
}


path: update.sh
content: #!/usr/bin/env bash
# surf_query skill 自動更新腳本
# 由 OpenClaw 每週維護 cron 呼叫，或手動執行

SKILL_DIR="$HOME/.openclaw/skills/surf_query"
REPO_URL="https://github.com/Harperbot/openclaw-surf-query"
CURRENT="$SKILL_DIR/surf_query.py"
REMOTE="$REPO_URL/raw/main/surf_query.py"

echo "[surf_query] 檢查更新..."

REMOTE_VER=$(curl -sf "$REMOTE" | grep -m1 '# version:' | awk '{print $3}')
LOCAL_VER=$(grep -m1 '# version:' "$CURRENT" 2>/dev/null | awk '{print $3}')

if [ -z "$REMOTE_VER" ]; then
  echo "[surf_query] ⚠️ 無法取得遠端版本，跳過"
  exit 0
fi

if [ "$LOCAL_VER" = "$REMOTE_VER" ]; then
  echo "[surf_query] ✅ 已是最新版（$LOCAL_VER）"
  exit 0
fi

echo "[surf_query] 發現新版本：$LOCAL_VER → $REMOTE_VER，開始更新..."
curl -sf "$REPO_URL/raw/main/surf_query.py" -o "$CURRENT.tmp" && \
  mv "$CURRENT.tmp" "$CURRENT" && \
  curl -sf "$REPO_URL/raw/main/skill.yml" -o "$SKILL_DIR/skill.yml.tmp" && \
  mv "$SKILL_DIR/skill.yml.tmp" "$SKILL_DIR/skill.yml" && \
  curl -sf "$REPO_URL/raw/main/taiwan_surf_spots.json" -o "$SKILL_DIR/taiwan_surf_spots.json.tmp" && \
  mv "$SKILL_DIR/taiwan_surf_spots.json.tmp" "$SKILL_DIR/taiwan_surf_spots.json"

echo "[surf_query] ✅ 更新完成（$REMOTE_VER）"


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