🚌 臺北市公車
定點車機 AI Skill
串接臺北市定點車機即時 API 與 PTX 開放資料,提供公車即時位置查詢、 依站序精確 ETA 估算、路線搜尋、站序查詢、脫班偵測、 熱點站位與地圖視覺化功能。
📖 這個 Skill 能做什麼?
taipei-bus 是一個專為 OpenClaw Agent 設計的公車動態查詢技能,
串接三個核心資料來源,專注解決「公車多久到?」這個最常見的問題。
每筆含車牌、路綫、站點、方向
28,741 站全覆蓋,已本地快取
415 條公車路線完整去返程站序
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/
fetch API(Web Standard Fetch),
需要 Node.js 18 或更高版本。較舊版本請安裝 node-fetch 作為 polyfill。
⚡ 第一支公車查詢程式
前置需求
- Node.js 18.0+(含原生
fetch) - 已安裝本 Skill 並複製
lib/與data/目錄 - 網路連線(對外存取 PTX API + 車機 API)
基本查詢:234 線何時到站?
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 dynamic、bus ETA、Taipei 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
⚙️ 函式完整參考
所有函式以 require('./lib/bus-api.js') 引入
📡 即時車機函式(bus-api.js)
Promise<BusRecord[]> — 按更新時間倒序排列的公車陣列
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
| — | — | — | 無參數 |
範例
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));
Promise<BusRecord[]> — 該路線所有在線公車
參數
| 參數 | 類型 | 說明 |
|---|---|---|
routeId | string | TstBusEvent 路線代碼(如 '158701')⚠️ 非 PTX RouteID |
routeId 是 TstBusEvent 體系的代碼(如 158701),
不是 PTX RouteID(如 10132)。建議先用 searchRouteByName 確認。
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}`));
Promise<BusRecord | null> — 找不到時回傳 null
參數
| 參數 | 類型 | 說明 |
|---|---|---|
busId | string | 車牌號碼(如 'KKC-1100') |
const { findByBus, formatBus } = require('./lib/bus-api.js');
const bus = await findByBus('KKC-1100');
if (bus) {
console.log(formatBus(bus, true));
} else {
console.log('查無此車牌或車機未上線');
}
Promise<BusRecord[]> — CarOnStop === '1' 的公車陣列
參數
| 參數 | 類型 | 說明 |
|---|---|---|
stopId | string | 站點代碼(TstBusEvent StopID) |
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));
Promise<BusRecord[]> — 異常公車陣列,空陣列表示全系統正常
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
| — | — | — | 無參數 |
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)));
}
Promise<{[routeId]: {count, buses[]}}>
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)
Promise<EnrichedBusRecord[]> — 每筆記錄附加 _stopCoords 欄位
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)})`);
Promise<Array<{stopId, stopName, lat, lon, count}>>
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
topN | number | 10 | 回傳前 N 個熱點站 |
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)})`);
});
{name, lat, lon, stationId} | null
參數
| 參數 | 類型 | 說明 |
|---|---|---|
stopId | string | PTX StopID(如 '33210',⚠️ 不是 TstBusEvent StopID) |
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('找不到此站牌');
}
Array<{stopId, name, lat, lon...}>
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
keyword | string | — | 站名關鍵字 |
limit | number | 10 | 最多回傳筆數 |
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)
RouteInfo | null — 含 routeId, routeName, departure, terminal
參數
| 參數 | 類型 | 說明 |
|---|---|---|
name | string | 路線名(如 '234'、'南京'、'藍10') |
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}`);
}
RouteInfo | null
參數
| 參數 | 類型 | 說明 |
|---|---|---|
routeId | string | PTX RouteID(如 '10132') |
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
// 起點:… → 訖點:…
// 去程首班:… / 末班:…
RouteInfo[]
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
keyword | string | — | 關鍵字 |
limit | number | 10 | 最多回傳筆數 |
const { searchRoutes } = require('./lib/bus-api.js');
const routes = searchRoutes('藍', 5);
routes.forEach(r => console.log(`${r.routeName}: ${r.departure} → ${r.terminal}`));
StopEntry[] — 含 stopId, stopName, lat, lon
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
routeId | string | — | PTX RouteID |
direction | number | 0 | 0=去程,1=返程 |
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})`);
});
{minutes, remaining, total, stop, note}
參數
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
routeId | string | — | PTX RouteID |
targetStopId | string | — | 目標站 PTX StopID |
direction | number | 0 | 0=去程,1=返程 |
avgSecPerStop | number | 90 | 平均每站秒數(預設市區 1.5min) |
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} 站`);
{routeId, routeName, direction, index}[]
參數
| 參數 | 類型 | 說明 |
|---|---|---|
stopId | string | PTX StopID |
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 函式。
💡 五個實務情境
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 站(去程)
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();
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);
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('捷運公館');
// 使用 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 呼叫函式 → 回覆格式
searchRouteByName + etaBySequence→ 約 N 分鐘(含站序說明)
searchStops + findAtStation→ N 台停在站位(車牌列表)
findByBus + formatBus→ 車牌/路線/站點/方向/更新時間
getHotspots→ TOP N 熱點站(站名 + 車數 + 座標)
searchRouteByName + getRouteStopSequence→ 完整站名列表(去程/返程)
detectAnomalies→ ✅ 全正常 或 ⚠️ N台異常(含明細)
searchRouteByName + getRouteInfo→ 去程/返程首末班時間
「我在 XX 站,要去 OO 地址」的轉乘推薦,需要:
① 地址 geocoding(座標)→ ② 找最近站牌 → ③ 轉乘規劃。
目前 geocoding 與轉乘演算法尚未實作,
建議搭配 tencentmap-jsapi-gl-skill(騰訊地圖 geocoding API),
或使用 Google Maps / TPTP(大眾運輸)轉乘 API。
⚠️ 使用前請先了解
TstBusEvent 的 RouteID/StopID(如 158701、179647)
與 PTX 的 RouteID/StopID(如 10132、33210)
為獨立系統,兩者不相通。需透過「站名」或「路線名」搜尋建立對照。
建議用 searchRouteByName 取得 PTX RouteID 後再查站序。
etaBySequence),ETA 仍受限於:
① 車機只上報最近一站,非即時 GPS 軌跡;② 快取是靜態 snapshot,
站序實際可能因路況微調。建議將結果標記為「預估」而非「精確」。
tencentmap-jsapi-gl-skill 取得地址座標,
再配合公車站牌搜尋建立銜接,但複雜轉乘仍建議轉介 Google Maps / TPTP。
data/ 目錄)由
scripts/build-cache.py 產生,預設每季重建。
若 PTX 資料有更新(路線調整、站位搬遷),需手動執行重建腳本。
🌐 部署到 GitHub Pages
本文件網站為純靜態 HTML,可直接部署到 GitHub Pages、Netlify、Vercel 等任意靜態托管服務。 以下以 GitHub Pages 為例。
docs/ 目錄已包含所有 HTML、CSS、JS 檔案,
並推送到你的 GitHub 倉庫(需建立 gh-pages 分支或使用 main 分支)。
main branch,folder 選擇 /docs。
若直接上傳 docs 資料夾內容,選擇 main branch + / (root)。
https://<username>.github.io/<repo>/
即可看到文件網站。
docs/CNAME 檔案寫入你的網域名稱(如 taipei-bus.openclaw.ai),
並在 DNS 供應商設定 CNAME 指向 <username>.github.io。
本技能文件已設定 taipei-bus.openclaw.ai。
docs/ 目錄。
📄 授權與智財權
本 Skill 蒐集、整合的公車動態資料,依據政府資料開放授權條款(OGDL)提供。 可自由使用於非商業及商業用途,唯需標註資料來源為政府開放資料。
PTX 開放資料平台之資料依其使用規範,允許自由使用、修改與散布。 本 Skill 之 PTX 整合模組以 MIT License 授權。