v1.2.0 Node.js 18+ MIT / OGDL OpenClaw Skill

🚌 臺北市公車
定點車機 AI Skill

串接臺北市定點車機即時 API 與 PTX 開放資料,提供公車即時位置查詢、 依站序精確 ETA 估算、路線搜尋、站序查詢、脫班偵測、 熱點站位與地圖視覺化功能。

介紹

📖 這個 Skill 能做什麼?

taipei-bus 是一個專為 OpenClaw Agent 設計的公車動態查詢技能, 串接三個核心資料來源,專注解決「公車多久到?」這個最常見的問題。

📡
即時車機資料
臺北市定點車機 OD API
每筆含車牌、路綫、站點、方向
🗺️
站牌座標
PTX Bus Stop API
28,741 站全覆蓋,已本地快取
🚌
路線站序
PTX StopOfRoute API
415 條公車路線完整去返程站序
⏱️
三級 ETA 精度
站序版 ★★★★★ / Haversine ★★★ / Fallback ★
⚠️重要:兩套 ID 體系不相通

TstBusEvent 的 RouteID(如 158701)與 PTX 的 RouteID(如 10132,對應 234 線)為獨立系統, 兩者不相通。需透過「站名」或「路線名」搜尋建立對照。

安裝

📦 安裝方式

支援兩種安裝方式。方式一(SkillHub)是推薦方式,會自動處理所有依賴。

方式一:SkillHub 安裝(推薦)

終端機
skillhub_install install_skill taipei-bus

方式二:手動複製

lib/data/ 目錄手動複製到你的專案:

終端機
# 複製 lib 目錄( bus-api.js + route-api.js + stop-coords.js)
cp -r ~/.qclaw/skills/taipei-bus/lib ./taipei-bus-lib

# 複製快取資料(28,741 站座標 + 415 條路線站序)
cp -r ~/.qclaw/skills/taipei-bus/data ./taipei-bus-data

# 或完整複製整個 skill
cp -r ~/.qclaw/skills/taipei-bus /your/project/
💡 Node.js 18+ 需求:本 Skill 使用原生 fetch API(Web Standard Fetch), 需要 Node.js 18 或更高版本。較舊版本請安裝 node-fetch 作為 polyfill。
快速開始

⚡ 第一支公車查詢程式

前置需求

  • Node.js 18.0+(含原生 fetch
  • 已安裝本 Skill 並複製 lib/data/ 目錄
  • 網路連線(對外存取 PTX API + 車機 API)

基本查詢:234 線何時到站?

quickstart.js
const { searchRouteByName, etaBySequence, getRouteStopSequence } = require('./lib/bus-api.js');

async function main() {
  // Step 1:以路名搜尋(支援數字、幹線、顏色關鍵字)
  const route = searchRouteByName('234');
  if (!route) {
    console.log('查無此路線');
    return;
  }
  console.log(`找到:${route.routeName}(RouteID: ${route.routeId})`);

  // Step 2:取得去程站序
  const stops = getRouteStopSequence(route.routeId, 0); // 0=去程, 1=返程
  console.log(`共 ${stops.length} 站`);

  // Step 3:找「歡仔園」站(可用任意站名)
  const target = stops.find(s => s.stopName.includes('歡仔園'));
  if (!target) {
    console.log('站序中找不到目標站');
    return;
  }

  // Step 4:依站序估算(最精確版本)
  const eta = etaBySequence(route.routeId, target.stopId, 0);
  console.log(`\n🚌 ${route.routeName} → ${target.stopName}`);
  console.log(`⏱️  預估到站:${eta.minutes} 分鐘`);
  console.log(`📌 ${eta.note}`);
}

main().catch(console.error);

執行方式

終端機
node quickstart.js
# 輸出:
# 找到:234(RouteID: 10132)
# 共 40 站
# 🚌 234 → 歡仔園
# ⏱️  預估到站:8 分鐘
# 📌 依站序(第 12/40 站),離目標站尚有 5 站(去程)
觸發方式

🔔 AI Agent 觸發關鍵字

在 OpenClaw Agent 對話中提及以下關鍵字,即會啟用本 Skill

類別 觸發關鍵字 說明
動態 公車動態等公車公車到站公車多久公車即時 查詢公車到站時間
追蹤 公車追蹤公車來了公車怎麼還沒到 持續追蹤特定公車
地圖 公車地圖公車站牌公車熱點 地圖視覺化或站位查詢
路線 公車路線公車搜尋公車首班公車末班 路線資訊或搜尋
異常 公車脫班公車異常 脫班或異常偵測
英文 bus dynamicbus ETATaipei bus 英文觸發(混合對話)
資料來源

📡 四個 API 資料源

資料源 URL / 端點 內容說明 快取策略
即時
定點車機 OD
tcgbusfs.blob.core.windows.net/blobbus/TstBusEvent.json 車牌、路線代碼、站點代碼、行駛方向、更新時間、勤務狀態 無(每次即時抓取)
PTX Stop
站牌座標
ptx.transportdata.tw/MOTC/v2/Bus/Stop/City/Taipei 28,741 個站牌名稱、座標(lat/lon)、StationID data/stop-coords.json(~2 MB,每季重建)
PTX Route
路線清單
ptx.transportdata.tw/MOTC/v2/Bus/Route/City/Taipei 415 條公車路線名稱、起訖站、首末班、業者 data/route-full-cache.json(~11 MB,每季重建)
PTX StopOfRoute
站序
ptx.transportdata.tw/MOTC/v2/Bus/StopOfRoute/City/Taipei 各路線的去程/返程站序,含各站 StopID 與座標 data/route-full-cache.json(~11 MB,每季重建)
🔧 快取重建:快取由 scripts/build-cache.py 產生。 若 PTX 資料有更新,執行: python3 ~/.qclaw/skills/taipei-bus/scripts/build-cache.py
API 參考

⚙️ 函式完整參考

所有函式以 require('./lib/bus-api.js') 引入

📡 即時車機函式(bus-api.js)

fetchAll()
抓取全量即時公車資料(車機 OD)
bus-api async
回傳:Promise<BusRecord[]> — 按更新時間倒序排列的公車陣列

參數

參數類型預設值說明
無參數

範例

javascript
const { fetchAll } = require('./lib/bus-api.js');

const buses = await fetchAll();
console.log(`全系統共 ${buses.length} 台公車在線`);
buses.slice(0, 3).forEach(b => console.log(b.BusID, b.RouteID, b.StopID));
findByRoute(routeId)
依 TstBusEvent 路線代碼查詢所有在線公車
bus-api async
回傳:Promise<BusRecord[]> — 該路線所有在線公車

參數

參數類型說明
routeIdstringTstBusEvent 路線代碼(如 '158701')⚠️ 非 PTX RouteID
⚠️這裡的 routeId 是 TstBusEvent 體系的代碼(如 158701), 不是 PTX RouteID(如 10132)。建議先用 searchRouteByName 確認。
javascript
const { findByRoute } = require('./lib/bus-api.js');

const buses = await findByRoute('158701');
console.log(`路線 ${buses.length} 台公車在線`);
buses.forEach(b => console.log(`${b.BusID} 在 ${b.StopID}`));
findByBus(busId)
依車牌查單台公車即時狀態
bus-api async
回傳:Promise<BusRecord | null> — 找不到時回傳 null

參數

參數類型說明
busIdstring車牌號碼(如 'KKC-1100'
javascript
const { findByBus, formatBus } = require('./lib/bus-api.js');

const bus = await findByBus('KKC-1100');
if (bus) {
  console.log(formatBus(bus, true));
} else {
  console.log('查無此車牌或車機未上線');
}
findAtStation(stopId)
查某站目前停在站位的公車
bus-api async
回傳:Promise<BusRecord[]>CarOnStop === '1' 的公車陣列

參數

參數類型說明
stopIdstring站點代碼(TstBusEvent StopID)
javascript
const { findAtStation } = require('./lib/bus-api.js');

// 查 179647 站有幾台公車停在站位
const buses = await findAtStation('179647');
console.log(`站位 ${buses.length} 台停在站位`);
buses.forEach(b => console.log(b.BusID, b.RouteID));
detectAnomalies()
偵測脫班或狀態異常的公車(DutyStatus !== '1' 或 BusStatus !== '0')
bus-api async
回傳:Promise<BusRecord[]> — 異常公車陣列,空陣列表示全系統正常

參數

參數類型預設值說明
無參數
javascript
const { detectAnomalies, formatBus } = require('./lib/bus-api.js');

const anomalies = await detectAnomalies();
if (anomalies.length === 0) {
  console.log('✅ 全系統正常');
} else {
  console.log(`⚠️ ${anomalies.length} 台異常`);
  anomalies.forEach(b => console.log(formatBus(b)));
}
routeSummary()
全系統路線概況摘要(各路線目前車數)
bus-api async
回傳:Promise<{[routeId]: {count, buses[]}}>
javascript
const { routeSummary } = require('./lib/bus-api.js');

const summary = await routeSummary();
// 找出目前車數最多的前 5 條路線
const top5 = Object.entries(summary)
  .sort(([,a],[,b]) => b.count - a.count)
  .slice(0, 5);

top5.forEach(([routeId, {count}]) => {
  console.log(`路線 ${routeId}: ${count} 台`);
});

🗺️ PTX 座標整合函式(bus-api.js + stop-coords.js)

fetchAllWithCoords()
取得所有公車,並附加站牌座標(lat/lon/name)
bus-api async
回傳:Promise<EnrichedBusRecord[]> — 每筆記錄附加 _stopCoords 欄位
javascript
const { fetchAllWithCoords } = require('./lib/bus-api.js');

const buses = await fetchAllWithCoords();
const first = buses[0];
console.log(first.BusID, first.RouteID);
console.log('📍', first._stopCoords?.name,
  `(${first._stopCoords?.lat?.toFixed(5)}, ${first._stopCoords?.lon?.toFixed(5)})`);
getHotspots(topN)
取得全系統熱點站位(目前公車密度最高的 N 個站)
bus-api async
回傳:Promise<Array<{stopId, stopName, lat, lon, count}>>

參數

參數類型預設值說明
topNnumber10回傳前 N 個熱點站
javascript
const { getHotspots } = require('./lib/bus-api.js');

const hotspots = await getHotspots(5);
hotspots.forEach((h, i) => {
  console.log(`${i+1}. ${h.stopName} — ${h.count} 台`);
  console.log(`   📍 (${h.lat.toFixed(5)}, ${h.lon.toFixed(5)})`);
});
getStopCoords(stopId)
依 PTX StopID 查站牌座標(同步,本地快取)
stop-coords sync
回傳:{name, lat, lon, stationId} | null

參數

參數類型說明
stopIdstringPTX StopID(如 '33210',⚠️ 不是 TstBusEvent StopID)
javascript
const { getStopCoords } = require('./lib/bus-api.js');

const coords = getStopCoords('33210');
if (coords) {
  console.log(coords.name); // "公館"
  console.log(coords.lat, coords.lon);
} else {
  console.log('找不到此站牌');
}
searchStops(keyword, limit)
以站名模糊搜尋站牌(同步,本地快取)
stop-coords sync
回傳:Array<{stopId, name, lat, lon...}>

參數

參數類型預設值說明
keywordstring站名關鍵字
limitnumber10最多回傳筆數
javascript
const { searchStops } = require('./lib/bus-api.js');

const hits = searchStops('捷運', 5);
hits.forEach(s => {
  console.log(`${s.stopId}: ${s.name}`);
  console.log(`  📍 (${s.lat?.toFixed(5)}, ${s.lon?.toFixed(5)})`);
});

🚌 PTX 路線整合函式(bus-api.js + route-api.js)

searchRouteByName(name)
以路名搜尋公車路線(精確 + 模糊匹配)
route-api sync
回傳:RouteInfo | null — 含 routeId, routeName, departure, terminal

參數

參數類型說明
namestring路線名(如 '234''南京''藍10'
javascript
const { searchRouteByName } = require('./lib/bus-api.js');

const route = searchRouteByName('藍10');
// 或 const route = searchRouteByName('0南');
if (route) {
  console.log(`${route.routeName}(${route.routeId})`);
  console.log(`${route.departure} → ${route.terminal}`);
}
getRouteInfo(routeId)
以 PTX RouteID 查完整路線資訊(含首末班、業者)
route-api sync
回傳:RouteInfo | null

參數

參數類型說明
routeIdstringPTX RouteID(如 '10132'
javascript
const { getRouteInfo } = require('./lib/bus-api.js');
const { routeApi } = require('./lib/bus-api.js');

const route = getRouteInfo('10132'); // 234線
console.log(routeApi.formatRoute(route));
// 輸出:
// 🚌 路線:234(234)
//    代碼:10132
//    起點:… → 訖點:…
//    去程首班:… / 末班:…
searchRoutes(keyword, limit)
模糊搜尋所有匹配路線(可回傳多筆)
route-api sync
回傳:RouteInfo[]

參數

參數類型預設值說明
keywordstring關鍵字
limitnumber10最多回傳筆數
javascript
const { searchRoutes } = require('./lib/bus-api.js');

const routes = searchRoutes('藍', 5);
routes.forEach(r => console.log(`${r.routeName}: ${r.departure} → ${r.terminal}`));
getRouteStopSequence(routeId, direction)
取得某路線的全部站序(去程或返程)
route-api sync
回傳:StopEntry[] — 含 stopId, stopName, lat, lon

參數

參數類型預設值說明
routeIdstringPTX RouteID
directionnumber00=去程,1=返程
javascript
const { getRouteStopSequence } = require('./lib/bus-api.js');

const stops = getRouteStopSequence('10132', 0); // 234去程
stops.forEach((s, i) => {
  console.log(`${i+1}. ${s.stopName}(${s.stopId})`);
});
etaBySequence(routeId, targetStopId, direction, avgSecPerStop)
依站序估算公車到站時間(最精確版本)
route-api sync
回傳:{minutes, remaining, total, stop, note}

參數

參數類型預設值說明
routeIdstringPTX RouteID
targetStopIdstring目標站 PTX StopID
directionnumber00=去程,1=返程
avgSecPerStopnumber90平均每站秒數(預設市區 1.5min)
javascript
const { etaBySequence } = require('./lib/bus-api.js');

// 234線 → 歡仔園站(StopID='...需查詢...')
const eta = etaBySequence('10132', '33210', 0);
console.log(`預估到站:${eta.minutes} 分鐘(${eta.note})`);
console.log(`尚有 ${eta.remaining}/${eta.total} 站`);
getRoutesByStop(stopId)
查某站點有哪些路線經過
route-api sync
回傳:{routeId, routeName, direction, index}[]

參數

參數類型說明
stopIdstringPTX StopID
javascript
const { getRoutesByStop } = require('./lib/bus-api.js');

const routes = getRoutesByStop('33210');
routes.forEach(r => {
  const dir = r.direction === 0 ? '去程' : '返程';
  console.log(`${r.routeName}(${dir})站序第 ${r.index} 站`);
});

⏱️ ETA 精度三級系統

等級 計算方式 精度 適用情境
★★★★★
站序版
剩餘站數 × 90s ±1-2 站 已知公車位置 + 完整站序時(etaBySequence
★★★
Haversine
直線距離 ÷ 25 km/h ±2-5 分鐘 有公車座標 + 站牌座標時(etaEstimate

Fallback
500m ÷ 25 km/h = 1.2 分 粗估 無座標時(固定站距估算)

最高精度需要:知道公車目前在哪一站 × 該路線的完整站序。 這也是為什麼 etaBySequence 是最推薦的 ETA 函式。

實用範例

💡 五個實務情境

1
查公車到站時間
以站序估算特定路線到特定站點的時間
eta-example.js
const { searchRouteByName, etaBySequence, getRouteStopSequence } = require('./lib/bus-api.js');

async function getBusEta(routeName, targetStopName) {
  const route = searchRouteByName(routeName);
  if (!route) return `查無「${routeName}」路線`;

  const stops = getRouteStopSequence(route.routeId, 0);
  const target = stops.find(s => s.stopName.includes(targetStopName));
  if (!target) return `站序中找不到「${targetStopName}」`;

  const eta = etaBySequence(route.routeId, target.stopId, 0);
  return `🚌 ${route.routeName} → ${target.stopName}:約 ${eta.minutes} 分鐘\n${eta.note}`;
}

await getBusEta('234', '歡仔園');
// 🚌 234 → 歡仔園:約 8 分鐘
// 依站序(第 12/40 站),離目標站尚有 5 站(去程)
2
脫班異常偵測
全系統掃描,找出勤務或狀態異常的公車
anomaly-example.js
const { detectAnomalies, formatBus } = require('./lib/bus-api.js');

async function checkSystemHealth() {
  const anomalies = await detectAnomalies();
  if (anomalies.length === 0) return '✅ 目前全系統公車正常';

  const lines = [`⚠️  共 ${anomalies.length} 台異常:\n`];
  anomalies.forEach(b => {
    const reasons = [];
    if (b.DutyStatus !== '1') reasons.push(`勤務(${b.DutyStatus})`);
    if (b.BusStatus !== '0') reasons.push(`狀態(${b.BusStatus})`);
    lines.push(`🚨 ${b.BusID} 路線:${b.RouteID} → ${reasons.join('、')}`);
  });
  return lines.join('\n');
}

await checkSystemHealth();
3
站序查詢
查詢特定路線的完整去程/返程站序
route-sequence.js
const { searchRouteByName, getRouteStopSequence } = require('./lib/bus-api.js');

function showRouteStops(routeName, direction = 0) {
  const route = searchRouteByName(routeName);
  if (!route) return;
  const dirLabel = direction === 0 ? '去程' : '返程';
  const stops = getRouteStopSequence(route.routeId, direction);
  console.log(`🚌 ${route.routeName} ${dirLabel}(共 ${stops.length} 站)\n`);
  stops.forEach((s, i) => console.log(`  ${String(i+1).padStart(2,'0')}. ${s.stopName}`));
}

showRouteStops('藍10', 0);
4
站名搜尋 + 經過路線
輸入站名模糊搜尋,再查所有經過該站的公車路線
stop-search.js
const { searchStops, getRoutesByStop } = require('./lib/bus-api.js');

async function findBusRoutesAtStop(stopKeyword) {
  const hits = searchStops(stopKeyword, 3);
  if (!hits.length) return `找不到含「${stopKeyword}」的站牌`;

  const results = [];
  for (const stop of hits) {
    const routes = getRoutesByStop(stop.stopId);
    results.push({
      stop: stop.name,
      routes: routes.map(r => `${r.routeName}(${r.direction===0?'去':'返'}第${r.index}站)`)
    });
  }
  return results;
}

await findBusRoutesAtStop('捷運公館');
5
Cron Job 定時監控
設定每 N 分鐘自動偵測脫班並推播通知
使用 cron tool
// 使用 qclaw-cron-skill 設定每 5 分鐘檢查一次
cron(action='add', job={
  name: '臺北市公車脫班偵測',
  schedule: { kind: 'every', everyMs: 5 * 60 * 1000 },
  sessionTarget: 'isolated',
  payload: {
    kind: 'agentTurn',
    message: `執行 detectAnomalies() 並回報結果。若有異常,
              輸出「⚠️ N台異常:車牌/路線/原因」;若無異常,輸出「✅ 全系統正常」。`,
    timeoutSeconds: 60
  },
  delivery: { mode: 'announce' }
})

詳見 examples/example-cron.js,包含每 5 分鐘脫班偵測、 每日早晨路線概況回報、一次性到站提醒等多種 Cron Job 範本。

使用情境

💬 對話式使用情境

使用者提問 → Agent 呼叫函式 → 回覆格式

「234 多久會到?」
searchRouteByName + etaBySequence
→ 約 N 分鐘(含站序說明)
「XX 站有幾台公車?」
searchStops + findAtStation
→ N 台停在站位(車牌列表)
「查 KKC-1100 在哪?」
findByBus + formatBus
→ 車牌/路線/站點/方向/更新時間
「哪裡公車最多?」
getHotspots
→ TOP N 熱點站(站名 + 車數 + 座標)
「藍10的站序」
searchRouteByName + getRouteStopSequence
→ 完整站名列表(去程/返程)
「公車都正常嗎?」
detectAnomalies
→ ✅ 全正常 或 ⚠️ N台異常(含明細)
「234 的首班/末班?」
searchRouteByName + getRouteInfo
→ 去程/返程首末班時間
⚠️地址轉乘(部分限制)

「我在 XX 站,要去 OO 地址」的轉乘推薦,需要: ① 地址 geocoding(座標)→ ② 找最近站牌 → ③ 轉乘規劃。 目前 geocoding 與轉乘演算法尚未實作, 建議搭配 tencentmap-jsapi-gl-skill(騰訊地圖 geocoding API), 或使用 Google Maps / TPTP(大眾運輸)轉乘 API。

已知限制

⚠️ 使用前請先了解

1
⚠️ 兩套 ID 體系不相通
TstBusEvent 的 RouteID/StopID(如 158701179647) 與 PTX 的 RouteID/StopID(如 1013233210) 為獨立系統,兩者不相通。需透過「站名」或「路線名」搜尋建立對照。 建議用 searchRouteByName 取得 PTX RouteID 後再查站序。
2
ETA 精度有上限
即使用最高精度的站序版(etaBySequence),ETA 仍受限於: ① 車機只上報最近一站,非即時 GPS 軌跡;② 快取是靜態 snapshot, 站序實際可能因路況微調。建議將結果標記為「預估」而非「精確」。
3
即時資料僅涵蓋部分車機車輛
TstBusEvent API 只包含有安裝定點車機設備的公車,非全部營運車輛。 無車機的車輛不會出現在查詢結果中,不代表它不存在或未發車。
4
尚無轉乘推薦功能
從 A 點(地址)到 B 點(地址)的轉乘推薦,需要 geocoding + 轉乘 API, 目前未實作。可透過 tencentmap-jsapi-gl-skill 取得地址座標, 再配合公車站牌搜尋建立銜接,但複雜轉乘仍建議轉介 Google Maps / TPTP。
5
無擁擠度資料
目前 TstBusEvent API 不含乘客人數、擁擠程度資訊。 若需要乘車舒適度評估,需另行串接其他資料來源(如 TTIA 動態資訊或市府 Open Data)。
6
快取需定期重建
PTX 站牌座標與路線站序快取(data/ 目錄)由 scripts/build-cache.py 產生,預設每季重建。 若 PTX 資料有更新(路線調整、站位搬遷),需手動執行重建腳本。
部署

🌐 部署到 GitHub Pages

本文件網站為純靜態 HTML,可直接部署到 GitHub Pages、Netlify、Vercel 等任意靜態托管服務。 以下以 GitHub Pages 為例。

1
將 docs/ 目錄推送至 GitHub 倉庫
確保 docs/ 目錄已包含所有 HTML、CSS、JS 檔案, 並推送到你的 GitHub 倉庫(需建立 gh-pages 分支或使用 main 分支)。
2
Settings → Pages → Source
前往 Repository Settings → Pages,在 Source 選擇 main branch,folder 選擇 /docs。 若直接上傳 docs 資料夾內容,選擇 main branch + / (root)
3
等待 2-3 分鐘部署完成
GitHub 會自動編譯並發布。完成後存取 https://<username>.github.io/<repo>/ 即可看到文件網站。
4
(可選)設定自訂網域
docs/CNAME 檔案寫入你的網域名稱(如 taipei-bus.openclaw.ai), 並在 DNS 供應商設定 CNAME 指向 <username>.github.io。 本技能文件已設定 taipei-bus.openclaw.ai
💡 其他部署選項:除 GitHub Pages 外,亦可使用 Netlify(拖放上傳)、Vercel(CLI 部署), 或直接用任意 Web Server(Nginx、Caddy)托管 docs/ 目錄。
授權

📄 授權與智財權

📜
Skill 本體
OGDL(Open Government Data License)
本 Skill 蒐集、整合的公車動態資料,依據政府資料開放授權條款(OGDL)提供。 可自由使用於非商業及商業用途,唯需標註資料來源為政府開放資料。
🗺️
PTX 資料
MIT License
PTX 開放資料平台之資料依其使用規範,允許自由使用、修改與散布。 本 Skill 之 PTX 整合模組以 MIT License 授權。
📌 注意:本 Skill 由 OpenClaw Agent 社群維護, 非臺北市政府或 PTX 官方出品。如有公車動態資料疑問,請以官方資料為準。