首尔共享单车(따릉이)实时查询实战:k-skill seoul-bike 技能的 CLI 命令与 k-skill-proxy 架构解析
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
本指南围绕 k-skill 仓库中的seoul-bike技能展开,讲解如何通过 k-skill CLI 单入口脚本,经由k-skill-proxy代理服务器查询首尔共享单车(따릉이)的实时可借车辆数与空闲车桩数。读完本文,你将掌握nearby、search、realtime三个子命令的完整用法、proxy 三个 HTTP 端点的调用方式,以及从客户端脚本到代理服务器再到首尔开放数据平台的上游调用链与错误处理机制。
技能定位:做什么、何时用
seoul-bike是 k-skill 技能集中的一个「查询类」技能,功能定义见 skill.json(profiles为proxy+lookup)。它通过首尔开放数据广场(서울 열린데이터 광장)的 따릉이 实时租赁信息接口,汇总查询坐标周边或指定名称的租赁站当前「可借车辆数」和「空车桩数」。
适合以下典型对话场景:
- 「现在这里能借到 따릉이 吗?」
- 「光化门附近有空车桩吗?」
- 「江南站 따릉이 租赁站还剩几辆车?」
注意:该技能是纯查询用途,不包含任何预约或租车自动化能力(详见 instruction.md 的 Notes 章节)。
前置条件与环境变量
运行环境
- 只需 Python 3 标准库。查看入口脚本 seoul_bike.py 的导入列表,仅使用了
argparse、json、os、sys、urllib.error、urllib.parse、urllib.request、typing,无任何第三方依赖,可在任意 Python 3 环境直接运行。 - 可选的
KSKILL_PROXY_BASE_URL:仅在使用自托管(self-host)或其他独立代理时设置;留空则使用默认的 hosted 代理https://k-skill-proxy.nomadamas.org。
源码中get_proxy_base_url()(seoul_bike.py)对取值做了归一化:读取环境变量后先strip(),若为空字符串或占位符replace-me则回退到默认值,最终结果再去掉尾部/。测试用例test_proxy_base_url_defaults_to_hosted_proxy(test_seoul_bike.py)验证了在无环境变量时返回默认 hosted 代理地址。
环境变量要求:客户端零密钥
本技能没有必填的环境变量。用户无需自行申请首尔开放数据广场的 OpenAPI key。/v1/seoul-bike/*三个路由默认由 hosted proxy 调用,上游 key(SEOUL_OPEN_API_KEY)只保存在代理服务器端,客户端全程接触不到明文密钥——这正是「key 不出客户端、只存代理」的架构设计,具体策略参见 k-skill-proxy.md。
单一入口命令
技能统一通过 k-skill CLI 的exec子命令调用入口脚本:
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- <subcommand> [args]首次使用时,Agent 只需批准一次Bash(python3 *seoul_bike.py:*)模式的执行权限,此后对该脚本的调用都会自动放行。
--之后的位置即传给seoul_bike.py的参数。main()入口通过argparse的add_subparsers(dest="command", required=True)强制要求指定子命令(seoul_bike.py),缺省会直接报错退出。
子命令一览
| 命令 | 说明 |
|---|---|
nearby --lat LAT --lon LON [--radius-m 500] [--limit 10] [--json] | 查询指定坐标周边的实时租赁站 |
search <关键词> [--limit 10] [--json] | 在实时数据中按租赁站名称包含的关键词检索 |
realtime [--start-index 1 --end-index 1000] | 输出实时租赁信息原始 JSON 分页 |
各子命令的完整参数(含默认值)如下表,均来自 build_parser():
| 子命令 | 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
nearby | --lat | float | 必填 | 基准纬度 |
--lon | float | 必填 | 基准经度 | |
--radius-m | int | 500 | 搜索半径(米) | |
--limit | int | 10 | 最多返回的站点数 | |
--json | flag | 关 | 以 JSON 输出原始 payload | |
search | keyword | str | 必填 | 站点名称关键词 |
--start-index | int | 1 | 实时数据起始索引 | |
--end-index | int | 1000 | 首页结束索引;搜索会继续翻完全部分页 | |
--limit | int | 10 | 最多返回的匹配站点数 | |
--json | flag | 关 | 以 JSON 输出匹配结果 | |
realtime | --start-index | int | 1 | 起始索引 |
--end-index | int | 1000 | 结束索引 |
实战工作流
1. 查询当前位置周边租赁站
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- nearby --lat 37.5717 --lon 126.9763 --radius-m 500输出为逐行摘要,每个站点包含:
- 租赁站名称(대여소명)
- 可借车辆数(
parkingBikeTotCnt) - 空车桩数(
rackTotCnt - parkingBikeTotCnt) - 距离(米,仅 nearby 返回)
- 查询时刻(
proxy.requested_at)
对应的格式化逻辑见format_nearby()与format_station()(seoul_bike.py):首行输出站点总数与基准坐标/半径,随后逐站输出,末尾附带查询时刻。若车辆数或车桩数为空则显示「알 수 없음」而不是报错。
测试test_summarize_nearby_includes_bikes_docks_distance_and_timestamp(test_seoul_bike.py)验证了输出必须同时包含站点名、대여 가능 4대、빈 거치대 11개、距离(0m)与조회 시각时间戳。
加--json时直接输出代理返回的完整 JSON(含query、count、items、proxy元信息),便于程序化消费:
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- nearby --lat 37.5717 --lon 126.9763 --radius-m 500 --limit 2 --json2. 按租赁站名称搜索
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- search "광화문" --limit 5search的实现策略是先全量拉取、再本地过滤:cmd_search()调用fetch_realtime_payload()从--start-index起按--end-index作为页大小循环翻页,直到覆盖rentBikeStatus.list_total_count总数或拿到空页为止(seoul_bike.py);随后filter_realtime_rows()将关键词strip().lower()后对每个站点的名称做不区分大小写的包含匹配,达到--limit即停止(seoul_bike.py)。
测试test_search_fetches_all_realtime_pages_before_filtering(test_seoul_bike.py)通过 mockfetch_json模拟两页数据,验证了fetch_realtime_pages(1, 1)会把两页的行都收集回来且恰好调用两次接口。
无匹配时向 stderr 输出「'关键词'와 일치하는 따릉이 대여소가 없습니다.」并返回退出码 1;有匹配时按与 nearby 相同的格式输出并附带查询时刻。
3. 直接查看实时原文 JSON
npx -y @nomadamas/k-skill@0 exec seoul-bike scripts/seoul_bike.py -- realtime --start-index 1 --end-index 1000该命令等价于直接请求代理的/v1/seoul-bike/realtime端点,把上游bikeList的原始 JSON(含rentBikeStatus.row明细)原样打印,适合排查数据问题或做自定义分析。
底层调用链:客户端 → k-skill-proxy → 首尔开放数据广场
Proxy 三个端点
| 端点 | 上游数据集 | 说明 |
|---|---|---|
GET /v1/seoul-bike/realtime?startIndex=1&endIndex=1000 | 서울bikeList | 实时租赁信息原文 |
GET /v1/seoul-bike/stations?startIndex=1&endIndex=1000 | 서울tbCycleStationInfo | 租赁站主数据(master) |
GET /v1/seoul-bike/nearby?lat=37.5717&lon=126.9763&radius_m=500&limit=10 | — | 代理侧坐标周边过滤(内部先取全量实时数据) |
三个端点都已在 k-skill-proxy 中实现并注册,路由定义见 server.js;端点清单与上游 key 说明见 k-skill-proxy.md。
客户端脚本通过fetch_json()统一发起请求:urllib.parse.urlencode编码查询参数后拼接到{proxy_base_url}{path}?{query},带User-Agent: k-skill/seoul-bike请求头,超时 15 秒(TIMEOUT_SEC = 15)(seoul_bike.py)。
代理端的关键实现
上游请求构造在proxySeoulBikeDatasetRequest()中(server.js):
${SEOUL_CITYDATA_BASE_URL}/${apiKey}/json/${dataset}/${startIndex}/${endIndex}/realtime端点对应dataset = "bikeList",stations端点对应dataset = "tbCycleStationInfo"(见proxySeoulBikeRealtimeRequest/proxySeoulBikeStationsRequest,server.js)。- 若服务器未配置
SEOUL_OPEN_API_KEY,代理直接返回 503,错误体为{"error": "upstream_not_configured", ...},不会请求上游。 - 成功响应会注入
proxy.requested_at = new Date().toISOString(),即查询时刻统一由代理服务器生成(requested_at字段来源)。 - 响应带内存缓存:
makeCacheKey依据路由与归一化参数生成 key,命中时返回proxy.cache.hit = true与ttl_ms;未命中且上游返回 2xx 时才写缓存(server.js)。 - 上游返回的 JSON 若命中语义错误(
getSeoulOpenApiSemanticError),代理统一回 502 并携带语义错误信息。 nearby端点在代理侧完成「拉全量 → 逐行归一化 → 过滤无坐标/超半径 → 按距离升序排序 → 截取 limit 条」的完整流程(server.js),客户端无需自行做地理计算。
字段归一化:两种命名都能消化
客户端normalize_realtime_row()(seoul_bike.py)对每个字段都兼容「上游 camelCase」与「代理 snake_case」两种键名,并做健壮的类型转换:
| 输出字段 | camelCase(上游) | snake_case(代理) | 计算 |
|---|---|---|---|
station_id | stationId | station_id | — |
station_name | stationName | station_name | — |
rack_total_count | rackTotCnt | rack_total_count | 字符串转 int |
available_bikes | parkingBikeTotCnt | available_bikes | 字符串转 int |
empty_docks | — | — | max(0, rack_total - available) |
shared_percent | shared | shared_percent | 字符串转 int |
latitude/longitude | stationLatitude/stationLongitude | latitude/longitude | — |
_to_int()会先把值转 float 再取整,并容忍空值;empty_docks用max(0, ...)保证不为负。测试test_search_realtime_filters_station_names_and_reports_empty_docks(test_seoul_bike.py)验证了 camelCase 输入能被正确换算为available_bikes = 4、empty_docks = 11。
错误处理与失败模式
入口脚本的main()对三类异常做了统一兜底(seoul_bike.py):
- HTTPError 503 +
upstream_not_configured:输出「k-skill-proxy에 필요한 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요.」——对应代理端未配置SEOUL_OPEN_API_KEY。 - 其他 HTTPError:优先输出代理返回的
message字段,否则输出API HTTP 오류: {code} {reason}。 - URLError(代理不可达):输出「설정된 k-skill-proxy 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요.」并附原因。
- JSONDecodeError:输出「API 응답 JSON 파싱 실패」并附解析异常信息。
instruction.md 列出的失败模式与之对应:
- 代理上游 key 未设置(缺少
SEOUL_OPEN_API_KEY)→ 客户端收到 503 提示; - 首尔开放数据广场 quota 超限 → 上游语义错误,代理回 502;
- 实时 API 返回空行或临时错误 → 输出为空或解析失败,需要重试;
- 坐标缺失或半径内无租赁站 → nearby 返回
count: 0,search 返回空并给出「无匹配」提示。
完成标准
一次成功的查询应答应满足:
- 已汇总可借车辆数与空车桩数;
- 明确标注基于 live data 的查询时刻(
proxy.requested_at); - 全程未向客户端暴露 upstream key。
使用注意事项
- 实时数据持续变化,回答时必须附带查询时刻,避免给用户造成「当前状态」的错觉。
- 本技能是查询专用,不会执行预约/租车等写操作。
- 关于 proxy 的运维与更多环境变量配置,请参考 docs/features/k-skill-proxy.md;客户端与代理的环境变量约定(
KSKILL_PROXY_BASE_URL留空即用 hosted 代理)也在该文档中有系统说明。 - 想直接验证代理端可用性,可先请求
GET /health确认seoulBikeConfigured类健康指标,再调用业务端点(详见代理部署文档中的自检实践)。
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考