k-skill 的 seoul-bike(首尔따릉이)实时租赁点查询:基于 k-skill-proxy 的无密钥 API 架构与 CLI 实战指南
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
本篇技术指南围绕 k-skill 仓库中的seoul-bike(首尔公共自行车 따릉이)技能展开,讲解如何通过k-skill-proxy以"零客户端密钥"的方式查询首尔 따릉이 实时租赁点的可租自行车数量、空置停车桩数量,并支持按坐标半径或站名关键词检索。读完本文,你将掌握GET /v1/seoul-bike/{realtime,stations,nearby}三个代理路由的调用方式、seoul_bike.py单入口 CLI 的全部子命令与参数、代理服务端缓存的元数据结构,以及异常场景下的错误处理与测试验证方法。
这个功能能做什么
- 查询当前坐标周边 따릉이 租赁点的可租自行车数量(
parkingBikeTotCnt); - 计算并返回空置停车桩数量(
rackTotCnt - parkingBikeTotCnt); - 支持按租赁点名称关键词搜索实时状态(如"광화문");
- 无需用户自行申请首尔开放数据广场(서울 열린데이터 광장)的
SEOUL_OPEN_API_KEY,全部经k-skill-proxy代理完成,upstream key 只存放在代理服务器端。
技能元数据在 seoul-bike/skill.json 中定义为category: transit、locale: ko-KR、phase: v1,说明该技能定位为面向韩国本地场景的交通类实时查询能力。
前置条件:零密钥设计的前提
在使用前,建议先阅读 公共设置指南 了解 k-skill 整体的环境变量与密钥解析顺序。对于seoul-bike技能而言,前置条件非常轻量:
- 客户端仅依赖 Python 3 标准库(
argparse、json、os、urllib.*),无需安装任何第三方依赖; - 无需任何必填环境变量。用户不需要亲自申请 서울 열린데이터 광장 OpenAPI key,因为
/v1/seoul-bike/*路由默认由 hosted 代理调用,upstream key 仅保存在代理服务器端; - 可选的
KSKILL_PROXY_BASE_URL仅在自托管(self-host)或使用其他代理时设置;留空时使用默认 hosted 地址https://k-skill-proxy.nomadamas.org。
KSKILL_PROXY_BASE_URL 的解析逻辑
源码 seoul-bike/scripts/seoul_bike.py 中get_proxy_base_url()的解析规则为:
value = os.environ.get("KSKILL_PROXY_BASE_URL") if value and value.strip() and value.strip() != "replace-me": return value.strip().rstrip("/") return DEFAULT_PROXY_BASE_URL # https://k-skill-proxy.nomadamas.org注意两个细节:
- 空值、纯空白以及占位符
replace-me都会被忽略并回退到默认 hosted 地址; - 返回值会去掉末尾的
/,避免与后续路径拼接时出现双斜杠。
这与 docs/setup.md 中secrets.env里KSKILL_PROXY_BASE_URL=留空的推荐做法完全一致:普通用户留空即使用 hosted 代理,只有自托管代理运营者才需要填入自己的地址。
基本路径与 Proxy 路由
所有请求默认发往https://k-skill-proxy.nomadamas.org/v1/seoul-bike/*。三个路由的职责如下:
| endpoint | upstream / 行为 | 主要输入 |
|---|---|---|
GET /v1/seoul-bike/realtime | 서울 열린데이터 광장bikeList实时租赁信息页 | startIndex、endIndex |
GET /v1/seoul-bike/stations | 서울 열린데이터 광장tbCycleStationInfo租赁点主数据页 | startIndex、endIndex |
GET /v1/seoul-bike/nearby | 代理服务端对 realtime 行按坐标半径过滤 | lat、lon、radius_m、limit |
代理服务端实现(server.js)
在代理源码 packages/k-skill-proxy/src/server.js 中可以印证以上三个路由的具体行为:
- realtime / stations:先经
normalizeSeoulBikePageQuery校验并规范化查询参数(非法输入返回400 bad_request),然后以config.seoulOpenApiKey注入 upstream 请求;响应会解析 JSON 并通过getSeoulOpenApiKey语义错误检测,最后在 payload 上附加proxy元数据(name、cache、requested_at),成功响应(2xx)会按 TTL 写入缓存。 - nearby:代理服务端先调用
fetchAllSeoulBikeRealtimeRows拉取全部实时行,再在服务端完成"归一化 → 过滤坐标缺失行 → 按distance_m <= radiusMeters过滤 → 按距离升序排序 →slice(0, limit)截断"的完整流水线,最后构造{query, count, items, proxy}响应结构。这意味着半径过滤与排序完全在代理端完成,客户端拿到即为按距离排序的最终结果。
三个路由都实现了makeCacheKey缓存的读取与回写:缓存命中时返回proxy.cache.hit: true,未命中时回写并附带ttl_ms。这一点在"响应结构"一节还会细说。
基本请求流程
- 客户端/技能调用默认 hosted 路径或
KSKILL_PROXY_BASE_URL下的/v1/seoul-bike/nearby端点; - 代理使用服务器端
SEOUL_OPEN_API_KEY调用 서울 열린데이터 광장bikeList; - 代理按坐标与半径对租赁点排序,返回
available_bikes、empty_docks、distance_m; - 响应附带
proxy.cache.hit、proxy.requested_at元数据。
curl 直接调用示例
BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}" curl -fsS --get "${BASE}/v1/seoul-bike/nearby" \ --data-urlencode 'lat=37.5717' \ --data-urlencode 'lon=126.9763' \ --data-urlencode 'radius_m=500' \ --data-urlencode 'limit=5'BASE变量使用了${KSKILL_PROXY_BASE_URL:-默认值}展开,与源码中get_proxy_base_url()的默认行为保持一致。其他两个路由同样可直接调用,例如:
curl -fsS --get "${BASE}/v1/seoul-bike/realtime" \ --data-urlencode 'startIndex=1' \ --data-urlencode 'endIndex=1000'CLI 使用:单入口脚本
技能采用单一入口点设计,统一通过 k-skill CLI 执行:
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- <subcommand> [args]首次使用时批准Bash(python3 *seoul_bike.py:*)这一条命令模式后,后续调用会自动放行。脚本本身是一个完整的argparse程序(seoul-bike/scripts/seoul_bike.py),子命令与参数如下:
| 子命令 | 参数与默认值 | 说明 |
|---|---|---|
nearby | --lat(必填)、--lon(必填)、--radius-m 500、--limit 10、--json | 查询坐标周边实时租赁点 |
search | <keyword>(必填)、--start-index 1、--end-index 1000、--limit 10、--json | 按租赁点名称关键词搜索实时状态 |
realtime | --start-index 1、--end-index 1000 | 查询实时租赁信息原始 JSON 页 |
示例 1:坐标周边查询
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- nearby --lat 37.5717 --lon 126.9763 --radius-m 500输出摘要包含:租赁点名称、可租自行车数、空置停车桩数、距离(米)、查询时间(proxy.requested_at)。预期输出格式如下:
따릉이 주변 대여소 2곳 기준 좌표: 37.5717, 126.9763 / 반경 500m - 101. 광화문역 1번출구 앞: 대여 가능 4대, 빈 거치대 11개, 거리 0m 조회 시각: 2026-05-21T06:10:00.000Z示例 2:按名称搜索
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- search "광화문" --limit 5search的底层行为值得注意:fetch_realtime_payload()(seoul-bike/scripts/seoul_bike.py)会自动按list_total_count翻页拉取全部实时行,直到current_end >= total_count或该页无行才停止,然后再用关键词做大小写不敏感的包含匹配(filter_realtime_rows)。也就是说,即使--end-index 1000指定的首页只有 1000 行,只要list_total_count更大,脚本会继续请求后续页直到拉完整个数据集,避免漏检。找不到匹配时向stderr输出提示并返回退出码1。
字段归一化与空值兜底
normalize_realtime_row()(seoul-bike/scripts/seoul_bike.py)同时兼容 upstream 韩文/英文两套字段名:
| 统一字段 | upstream 候选字段 |
|---|---|
station_id | stationId/station_id |
station_name | stationName/station_name |
rack_total_count | rackTotCnt/rack_total_count |
available_bikes | parkingBikeTotCnt/available_bikes |
shared_percent | shared/shared_percent |
latitude/longitude | stationLatitude/latitude等 |
empty_docks由max(0, rack_total_count - available_bikes)计算;rack_total_count与available_bikes任一缺失时empty_docks为None,格式化输出会显示"알 수 없음"而非报错。字符串型数值(如"4")会经_to_int()安全转换为int。
响应结构与元数据
nearby响应的 JSON 结构为:
{ "query": { "latitude": 37.5717, "longitude": 126.9763, "radius_m": 500, "limit": 5 }, "count": 2, "items": [ { "station_id": "ST-101", "station_name": "101. 광화문역 1번출구 앞", "rack_total_count": 15, "available_bikes": 4, "empty_docks": 11, "shared_percent": 27, "distance_m": 0, "latitude": 37.5717, "longitude": 126.9763 } ], "proxy": { "name": "k-skill-proxy", "cache": { "hit": false, "ttl_ms": 30000 }, "requested_at": "2026-05-21T06:10:00.000Z" } }其中proxy.cache.hit标识本次响应是否来自代理缓存,proxy.requested_at为服务端发起请求的 ISO 时间戳。由于实时数据持续变化,向用户呈现结果时必须附带该查询时间,这正是脚本在format_nearby()中无条件追加"조회 시각"行的原因。
使用--json参数可直接输出原始 JSON(nearby、search均支持),方便集成到其他程序或进行调试。
fallback / 替代流程
- 显式设置
KSKILL_PROXY_BASE_URL时,优先使用该代理; - 默认 hosted 路径为
https://k-skill-proxy.nomadamas.org/v1/seoul-bike/*; - 自托管运营者只需在服务器端配置
SEOUL_OPEN_API_KEY(参见 k-skill 代理服务器指南 中SEOUL_OPEN_API_KEY=...一节),客户端无需任何密钥。
注意事项与失败模式
- 实时数据不断变化,回答中必须同时给出查询时间;
- 本技能仅用于查询,不进行预约/租赁自动化;
- 서울 열린데이터 광장 可能出现 quota 超限或临时故障;
- 半径内没有租赁点时,
items: []是正常返回,不代表报错。
CLI 层的错误处理
main()(seoul-bike/scripts/seoul_bike.py)对三类异常做了差异化处理:
HTTPError503 且响应体error == "upstream_not_configured"时,输出"代理未配置所需 API 密钥,请联系运营者";- 其余 HTTP 错误优先输出代理返回的
message,否则输出API HTTP 오류: <code> <reason>; URLError(代理不可达)输出"代理服务器无响应,请稍后重试或联系运营者";- JSON 解析失败则输出解析错误信息。
对应的失败场景在 seoul-bike/instruction.md 中归纳为:代理 upstream key 未设置(无SEOUL_OPEN_API_KEY)、서울 열린데이터 광장 quota 超限、实时 API 返回空行或临时错误、坐标缺失或半径内无租赁点。
测试验证
仓库在 scripts/test_seoul_bike.py 中提供了基于unittest的测试,覆盖了核心行为:
format_nearby输出包含站名、可租数、空桩数、距离与查询时间;filter_realtime_rows能按关键词过滤并按empty_docks正确计算;- 分页行为:mock
fetch_json返回两页数据时,fetch_realtime_pages会连续请求 2 次并合并两页结果(对应list_total_count翻页逻辑); - CLI 层
search/nearby输出与--json输出结构正确; - 清空环境变量后
get_proxy_base_url()回退到 hosted 默认地址。
这些测试既验证了脚本的字段归一化、分页抓取、格式化输出,也印证了本文前述的默认代理地址与响应结构,可作为二次开发时的回归保障。
相关文档与源码
- 技能说明:seoul-bike/SKILL.md、seoul-bike/instruction.md
- 核心脚本:seoul-bike/scripts/seoul_bike.py
- 代理服务端路由:packages/k-skill-proxy/src/server.js
- 代理部署与密钥管理:docs/features/k-skill-proxy.md、docs/setup.md
- 测试用例:scripts/test_seoul_bike.py
参考表面(서울 열린데이터 광장):bikeList(따릉이 실시간 대여정보)、tbCycleStationInfo(따릉이 대여소 정보)。
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考