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
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' 的公交车数组
javascript
const { findAtStation } = require('./lib/bus-api.js');

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();
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
javascript
const { getStopCoords } = require('./lib/bus-api.js');

const coords = getStopCoords('33210');
if (coords) {
  console.log(coords.name); // "公馆"
  console.log(coords.lat, coords.lon);
}
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
javascript
const { searchRouteByName } = require('./lib/bus-api.js');

const route = searchRouteByName('蓝10');
if (route) {
  console.log(`${route.routeName}(${route.routeId})`);
  console.log(`${route.departure} → ${route.terminal}`);
}
getRouteInfo(routeId)
按 PTX RouteID 查完整路线信息(含首末班、业者)
route-api sync
返回:RouteInfo | null
javascript
const { getRouteInfo, routeApi } = require('./lib/bus-api.js');

const route = getRouteInfo('10132'); // 234线
console.log(routeApi.formatRoute(route));
searchRoutes(keyword, limit)
模糊搜索所有匹配路线(可返回多条)
route-api sync
返回:RouteInfo[]
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');

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}[]
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 分 粗估 无坐标时(固定站距估算)
实用示例

💡 五个实务场景

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', '欢仔园');
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) 为独立系统,两者不相通。需通过「站名」或「路线名」搜索建立对照。
2
ETA 精度有上限
即使用最高精度的站序版(etaBySequence),ETA 仍受限於: ① 车机只上报最近一站,非实时 GPS 轨迹;② 缓存是静态 snapshot, 站序实际可能因路况微调。建议将结果标记为「预估」而非「精确」。
3
实时数据仅涵盖部分车机车辆
TstBusEvent API 只包含有安装定点车机设备的公交车,非全部运营车辆。 无车机的车辆不会出现在查询结果中,不代表它不存在或未发车。
4
尚无换乘推荐功能
从 A 点(地址)到 B 点(地址)的换乘推荐,需要 geocoding + 换乘 API, 目前未实现。可通过 tencentmap-jsapi-gl-skill 取得地址坐标, 再配合公交车站点搜索建立衔接,但复杂换乘仍建议转介 Google Maps / TPTP。
5
无拥挤度数据
目前 TstBusEvent API 不含乘车人数、拥挤程度信息。 若需要乘车舒适度评估,需另行接入其他数据来源。
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
3
等待 2-3 分钟部署完成
GitHub 会自动编译并发布。完成后访问 https://<username>.github.io/<repo>/ 即可看到文档网站。
4
(可选)设定自定义域名
docs/CNAME 文件写入你的域名(如 taipei-bus.openclaw.ai), 并在 DNS 供应商设定 CNAME 指向 <username>.github.io
授权

📄 授权与知识产权

📜
Skill 本体
OGDL(Open Government Data License)
本 Skill 收集、整合的公交车动态数据,依据政府资料开放授权条款(OGDL)提供。 可自由使用于非商业及商业用途,唯需标注资料来源为政府开放资料。
🗺️
PTX 数据
MIT License
PTX 开放资料平台之数据依其使用规范,允许自由使用、修改与散布。 本 Skill 之 PTX 整合模组以 MIT License 授权。
📌 注意:本 Skill 由 OpenClaw Agent 社群维护, 非台北市政府或 PTX 官方出品。如有公交车动态数据疑问,请以官方资料为准。