cheap-gas-nearby 实战指南:基于韩国 Opinet 官方 API 的附近最便宜加油站查询包
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
cheap-gas-nearby是 k-skill 仓库中面向韩国场景发布的 Node.js 包(v0.4.0,MIT 协议),它以 한국석유공사(韩国石油公社)Opinet 官方开放 API 为价格数据源,将「동네/역명/랜드마크(小区/地铁站名/地标)」这类自然语言位置串通过 Kakao Map 锚点搜索解析为 WGS84 坐标,再经 WGS84→KATEC 坐标变换接入 Opinet 周边加油站检索,最终按「价格优先、距离次之」返回排序结果。读完本文,你将掌握该包的安装方式、完整调用链、9 个公开 API 的用途与源码实现细节,并能在自己的 Node.js(≥18)项目或 AI Agent 技能中直接落地「查附近最便宜的加油站」这一实战能力。
一、包定位与使用原则
cheap-gas-nearby的核心职责非常聚焦:用官方数据源回答「근처 가장 싼 주유소」(附近最便宜的加油站在哪)。它不是一个通用地图 SDK,而是一条经过封装的「位置→坐标→油价→排序」流水线,这在 README.md 的开篇就明确定位为:使用韩国石油公社 Opinet 官方 API 查找附近最便宜的加油站。
该包同时以cheap-gas-nearbySkill 的形式存在于仓库根目录的 cheap-gas-nearby/instruction.md 与 cheap-gas-nearby/SKILL.md 中,服务于 AI Agent 场景,并据此沉淀了四条硬性使用原则:
- 不自动追踪用户位置:包本身不会读取或猜测设备定位,Agent 必须先把「当前在哪」问清楚;
- 先问位置,再搜索:拿到 동네(小区)/ 역명(站名)/ 랜드마크(地标)/ 위도·경도(经纬度)中的任意一种格式后再动手;
- 价格数据优先走官方 Opinet Open API,保证数据权威性与合法性;
- 位置字符串先经 Kakao Map anchor 搜索拿坐标,再接 Opinet
aroundAll.do周边查询,两条链路分工明确。
其中关于「默认产品为 휘발유(汽油,B027),用户明确要 경유(柴油)才切换为 D047」的行为,也写进了 instruction.md 的应答策略中。
二、安装与运行环境
包在发布后可像普通 npm 包一样安装:
npm install cheap-gas-nearby若要在本仓库内直接开发、调试该包(例如配合 test/index.test.js 跑测试):
npm install运行环境要求可从 package.json 确认:
| 项目 | 值 |
|---|---|
| 版本 | 0.4.0 |
| Node.js 要求 | >=18(依赖全局fetch,见下文) |
| 入口文件 | src/index.js |
| 许可 | MIT |
| 发布文件 | src与README.md |
| 测试命令 | npm test(node --test) |
| Lint 命令 | npm run lint(node --check语法检查) |
需要特别说明的是,包的 HTTP 层在 src/index.js 中直接使用options.fetchImpl || global.fetch,因此必须运行在支持全局fetch的 Node 18+ 环境,或者通过fetchImpl注入自定义实现(测试正是利用这一注入点做 mock)。
三、整体架构与数据流
从 src/index.js 的常量与函数组织可以看出,整个查询是一条五段式流水线:
用户位置字符串(서울역 / 37.55472,126.97068) │ ▼ ① Kakao Map 锚点解析(仅字符串位置需要) m.map.kakao.com/actions/searchView?q=<query> → place-api.map.kakao.com/places/panel3/<confirmId> 取 WGS84 坐标 │ ▼ ② WGS84 → KATEC 坐标变换(parse.js 中的 wgs84ToKatec) │ ▼ ③ Opinet aroundAll.do 周边检索(sort=1 价格升序) │ ▼ ④ detailById.do 详情补全(地址/电话/自助/洗车/保养/品质认证) │ ▼ ⑤ 价格优先、距离次之排序 → 输出 limit 条结果这一链路在 instruction.md 的 Workflow 一节有等价描述,并在 test/index.test.js 中通过 mockfetchImpl一次串起了「서울역 搜索 HTML → 地点面板 JSON → aroundAll.do → detailById.do」的全部环节进行端到端验证。
值得强调的是第 ① 步的健壮性设计:resolveAnchor(src/index.js)会先对 Kakao 搜索结果按评分排序,然后逐个尝试候选地点的面板接口——若第一个候选的 panel 返回 404,或拿到的经纬度不是有限数值,就自动回退到下一个候选,全部失败才抛出No usable Kakao Map place panel was available for <query>。测试 index.test.js#L178-L254 专门验证了「首个候选无坐标时回退到第二候选」的行为。
四、官方 API 表面与关键参数
包所对接的官方接口在 README 与 instruction.md 中都有完整罗列,整理如下(均为包内部实际请求的真实端点):
| 用途 | 端点 |
|---|---|
| Opinet 开放 API 说明 | https://www.opinet.co.kr/user/custapi/openApiInfo.do |
| 半径内加油站列表 | https://www.opinet.co.kr/api/aroundAll.do |
| 加油站详情(按 ID) | https://www.opinet.co.kr/api/detailById.do |
| 地区代码 | https://www.opinet.co.kr/api/areaCode.do |
| Kakao Map 移动端锚点搜索 | https://m.map.kakao.com/actions/searchView?q=<query> |
| Kakao Map 地点面板 JSON | https://place-api.map.kakao.com/places/panel3/<confirmId> |
其中aroundAll.do的核心请求参数与取值范围,在buildAroundSearchParams(src/parse.js)中有强制校验,是调用该 API 的「契约」:
| 参数 | 含义 | 取值 / 约束 |
|---|---|---|
out | 返回格式 | 固定json |
x,y | 基准位置KATEC坐标 | 必须是有限数值,输出时保留 4 位小数 |
radius | 搜索半径(米) | 正数,最大 5000,默认 1000,超出即抛错 |
prodcd | 油品代码 | B027汽油(默认)、D047柴油、B034高标号汽油、C004煤油、K015LPG |
sort | 排序方式 | 1表示按价格排序 |
certkey | 官方 API 密钥 | 直连模式必填 |
油品代码到语义键的映射在 src/parse.js 中定义:B027→gasoline、B034→premiumGasoline、C004→kerosene、D047→diesel、K015→lpg,详情接口返回的多油品价格会按此映射到统一的prices对象中。
五、快速上手:按位置字符串查询
README 中的核心示例就是searchCheapGasStationsByLocationQuery——传一个「서울역」这样的自然语言位置,其余坐标解析全部自动完成:
const { searchCheapGasStationsByLocationQuery } = require("cheap-gas-nearby"); async function main() { const result = await searchCheapGasStationsByLocationQuery("서울역", { apiKey: process.env.OPINET_API_KEY, // 官方 API key,或走代理模式时可省略 radius: 1000, // 半径(米),默认 1000,最大 5000 productCode: "B027", // 油品代码,默认 B027(汽油) limit: 3 // 最多返回条数 }); console.log(result.anchor); // 锚点:name/sourceUrl/latitude/longitude 等 console.log(result.items); // 排序后的加油站列表 } main().catch((error) => { console.error(error); process.exitCode = 1; });该函数的实现(src/index.js)做了两件事:
- 先用
parseCoordinateQuery判断入参是否本身就是坐标——形如"37.55472,126.97068"(支持逗号、斜杠或空格分隔)的字符串会被直接识别为经纬度,从而跳过 Kakao 锚点搜索,直接走坐标查询; - 否则走
resolveAnchor完成位置→坐标解析,再调用坐标查询函数,并在返回结果中额外附带anchor、anchorCandidates以及meta.resolvedQuery(回填用户原始查询串)。
返回的result结构如下:
anchor:解析后的锚点对象,包含id、name、category、address、phone、latitude、longitude、sourceUrl(形如https://place.map.kakao.com/1001);items:按价格升序、距离次之排序的加油站数组,每个元素在detailLimit开启时还会合并详情字段(见第七节);meta:productCode、radius、total(候选总数),以及位置字符串模式下额外的resolvedQuery。
六、坐标直查:searchCheapGasStationsByCoordinates
如果调用方(如地图应用)已经持有用户经纬度,可以跳过锚点解析直接调用searchCheapGasStationsByCoordinates(src/index.js)。其可用选项与默认值如下:
| 选项 | 默认值 | 说明 |
|---|---|---|
latitude,longitude | 必填 | WGS84 经纬度,必须是有限数值 |
radius | 1000 | 搜索半径(米,≤5000) |
productCode | "B027" | 油品代码 |
sort | 1 | 排序方式(1 = 价格优先) |
limit | 5 | 返回条数,最小 1 |
detailLimit | 等于limit | 需要拉取详情的条数,最小 0(0 表示不拉详情) |
apiKey/certKey | 环境变量OPINET_API_KEY | 官方密钥(直连模式必填) |
limit与detailLimit都会经过normalizeCountOption(src/index.js)的严格校验:传入非有限数值会直接抛错(如limit must be a finite number)而不是静默返回空列表,这一点有专门的测试用例覆盖(index.test.js#L345-L367)。
函数内部流程为:WGS84→KATEC 变换 → 调aroundAll.do→sortStationsByPriceAndDistance排序 → 对前detailLimit条并发拉取详情(单个详情失败不会中断整体,而是以{ error }占位)→ 返回anchor(内含katecX/katecY)、items、meta三部分。
七、9 个公开 API 逐一解析
README「공개 API」一节列出了包的完整对外函数面,结合源码与测试,逐个说明其职责:
| API | 位置 | 职责与关键行为 |
|---|---|---|
parseSearchResultsHtml(html) | src/parse.js#L89-L116 | 解析 Kakao 移动搜索 HTML,抽取search_item base卡片中的id、name、category、address、phone |
selectAnchorCandidate(query, items) | src/parse.js#L180-L188 | 对候选按评分排序后取第一名,无候选则抛错 |
normalizeAnchorPanel(panel, searchItem) | src/parse.js#L190-L203 | 把 Kakao 地点面板 JSON 规整为统一锚点对象;point缺失时经纬度保持null而非强制 0(有测试专门守护该行为) |
wgs84ToKatec(latitude, longitude) | src/parse.js#L254-L294 | WGS84→Bessel→KATEC 坐标变换,返回{ x, y } |
buildAroundSearchParams(options) | src/parse.js#L306-L324 | 生成 OpinetaroundAll.do请求参数并做参数校验(半径、坐标合法性) |
parseAroundResponse(payload) | src/parse.js#L355-L359 | 解析RESULT.OIL列表(兼容数组/单对象),过滤掉无 ID 或价格非数值的脏数据 |
normalizeDetailItem(payload) | src/parse.js#L388-L418 | 规整详情:地址、电话、自助/洗车/保养/便利店/品质认证布尔位、按油品归类的prices与rawPrices |
searchCheapGasStationsByCoordinates(options) | src/index.js#L220-L272 | 坐标直查主入口(见第六节) |
searchCheapGasStationsByLocationQuery(locationQuery, options) | src/index.js#L274-L305 | 位置字符串查询主入口(见第五节) |
除此之外,src/index.js的module.exports还额外导出了fetchSearchResults、fetchPlacePanel、fetchDetailById、rankAnchorCandidates、sortStationsByPriceAndDistance、buildAroundSearchParams以及四个 URL 常量(AROUND_ALL_URL、DETAIL_BY_ID_URL、SEARCH_VIEW_URL、PLACE_PANEL_URL_BASE、DEFAULT_PROXY_BASE_URL),方便上层做更细粒度的组合与二次开发。
八、锚点候选的评分排序机制
位置字符串能自动选中最合适的锚点,依赖的是 src/parse.js#L118-L178 中的scoreAnchorCandidate评分体系。它对查询串与每个候选的名称/地址/分类做规范化(NFKC 归一化 + 去标点小写)后累加权重:
| 匹配条件 | 加分 |
|---|---|
| 名称与查询串完全一致 | +1000 |
| 名称恰为「查询串+역」或去掉「역」后一致 | +950 |
| 名称以查询串开头 | +800 |
| 名称包含查询串 | +600 |
| 地址包含查询串 | +120 |
| 名称/分类命中 역·기차역·광장·공원·랜드마크 等锚点模式 | +250 |
| 名称/分类是 주유소(加油站)本身 | -200(避免把结果站当锚点) |
| 候选 ID 非纯数字 | -500 |
| 分类含 기차역 / 전철역 | +80 |
同分时按韩文名称localeCompare(..., "ko")稳定排序。测试 index.test.js#L37-L42 验证了「서울역」能正确选中 id=1001 的 기차역 而非同名其他结果,index.test.js#L256-L343 则验证了「강남역」场景下首选候选 404 后,能按评分次序回退到「강남역 11번출구」的候选。
九、WGS84→KATEC 坐标变换原理
OpinetaroundAll.do要求 KATEC 坐标系下的x/y,而 Kakao Map 锚点面板返回的是 WGS84 经纬度,因此wgs84ToKatec(src/parse.js#L254-L294)是链路中不可省略的一环,其实现分两步:
- WGS84→Bessel 基准面转换:采用三参数平移(src/parse.js#L18 中的
WGS84_TO_BESSEL = [146.43, -507.89, -681.46]),配合 WGS84 与 Bessel 椭球长半轴/扁率,先用空间直角坐标平移得到 Bessel 经纬度(纬度用迭代法求解,收敛阈值1e-14,最多 8 次迭代); - Bessel→KATEC 投影:KATEC(Korea Transverse Mercator)以 38°N、128°E 为中央纬线/经线(
KATEC_LAT0/KATEC_LON0),假东 400000、假北 600000,比例因子 0.9999,经子午弧长展开式计算平面坐标(src/parse.js#L9-L18)。
测试 index.test.js#L72-L77 给出了可复现的基准值:(37.55472, 126.97068)转换结果为(309252.2237, 550779.9944)(容差 ±1),任何对该函数的改动都可以此回归校验。
十、两种数据获取模式:直连与代理
cheap-gas-nearby支持两条访问 Opinet 的路径,由 API key 的有无自动切换:
- 直连模式:当
options.apiKey/options.certKey或环境变量OPINET_API_KEY存在时(useDirectApi判定,见 src/index.js#L86-L88),包直接请求https://www.opinet.co.kr/api/aroundAll.do(拼接certkey)与detailById.do。此时若缺少密钥,会抛出OPINET_API_KEY or options.apiKey is required for official Opinet lookups.。 - 代理模式:没有密钥时,请求改走默认代理
https://k-skill-proxy.nomadamas.org(常量DEFAULT_PROXY_BASE_URL,可用options.proxyBaseUrl或环境变量KSKILL_PROXY_BASE_URL覆盖)下的/v1/opinet/around与/v1/opinet/detail。这一默认路径同样写入了 instruction.md:使用代理时用户侧无需自备OPINET_API_KEY,Agent 直接可用。
两种模式的切换点在fetchAroundStations与fetchDetailById(src/index.js#L164-L202)中,代理模式下参数名对齐为x/y/radius/prodcd/sort(/around)与id(/detail),返回结构由parseAroundResponse/normalizeDetailItem统一规整,对上层透明。
请求层还内置了贴近真实浏览器的请求头:普通文本请求使用带accept-language: ko的浏览器 UA 头;Kakao 地点面板请求额外携带origin、referer、appVersion: 6.6.0、sec-ch-ua等头(DEFAULT_PANEL_HEADERS);Opinet JSON 请求则使用轻量的 JSON 头(src/index.js#L19-L43)。所有请求都支持options.headers合并与options.signal超时控制。
十一、结果规范化:品牌映射与多油品价格
parseAroundResponse输出的每个周边条目含id、brandCode、brandName、name、price、distanceMeters、katecX、katecY。品牌代码到中文/韩文名称的映射表定义在 src/parse.js#L20-L31:
| 品牌代码 | 名称 |
|---|---|
SKE | SK에너지(SK 能源) |
GSC | GS칼텍스 |
HDO | 현대오일뱅크(现代 Oilbank) |
SOL | S-OIL |
E1G | E1 |
SKG | SK가스 |
NHO | 농협알뜰(农协实惠) |
RTE | 자영알뜰(个体实惠) |
RTX | 고속도로알뜰(高速实惠) |
ETC | 자가상표(自有品牌) |
详情条目(normalizeDetailItem)在周边信息之上追加:lotAddress/roadAddress(地籍/道路名地址)、phone、sigunCode、lpgYn、布尔型的isSelf(自助)、hasMaintenance(保养)、hasCarWash(洗车)、hasConvenienceStore(便利店)、kpetroCertified(品质认证),以及按PRODUCT_CODE_TO_KEY归类好的prices.gasoline/diesel/…和保留原始代码的rawPrices(含tradeDate/tradeTime成交时间)。mergeStationDetail(src/index.js#L204-L218)负责把两者合并,并保证合并后price、distanceMeters仍以周边数据为准。
排序函数sortStationsByPriceAndDistance(src/parse.js#L420-L432)的三级比较逻辑是:先比价格(升序),再比距离(升序),最后按韩文名排序。测试 index.test.js#L96-L108 验证了「A1000001(1635 韩元/112.4m)→ A1000003(1649 韩元/220m)→ A1000002(1649 韩元/315m)」这一价格优先、距离次之的期望顺序,其中后两者同价时 220m 排在 315m 前面。
十二、测试与可验证性
包的测试用 Node 内置的node:test编写(npm test即可运行),测试文件 test/index.test.js 与 test/fixtures 目录下的固定样本(anchor-search.html、anchor-panel.json、around-response.json、detail-a1000001.json、detail-a1000003.json)共同覆盖了:
- HTML 搜索卡片解析与锚点选优(
parseSearchResultsHtml/selectAnchorCandidate); - 锚点面板规整与坐标缺失时的
null保持; - WGS84→KATEC 数值基准(309252.2237, 550779.9944);
aroundAll.do参数编码契约;- 周边结果价格排序与详情字段补全(自助、洗车、保养、品质认证、多油品价格);
- 带 mock fetch 的端到端调用链、锚点回退、评分序回退、非法
limit拒绝。
这些测试既是行为规格,也是读者验证「包工作正常」的最直接手段。
十三、失败模式与使用注意事项
结合 instruction.md 的 Failure modes 一节与源码中的防御逻辑,实际使用中需要关注以下风险点:
- 密钥缺失:直连模式必须有
OPINET_API_KEY(或apiKey/certKey),否则抛错;代理模式下则要求代理服务可用且服务端已配置密钥; - Kakao 锚点歧义:位置串模糊时可能选中错误坐标,包会用评分与候选回退尽量缓解,但仍建议 Agent 在结果可疑时向用户二次确认位置;
- Opinet 响应波动:官方 API 可能出现临时空结果或数据更新中,
parseAroundResponse会过滤无价格记录,此时应如实告知「没找到」及下一步追问,而不是编造结果; - 参数边界:
radius超过 5000 米、坐标为 NaN、limit/detailLimit非有限数值都会立即抛错,属于设计上的「快速失败」策略。
对于 Agent 场景,searchCheapGasStationsByLocationQuery的使用纪律是:先问清当前位置与所需油品,再调用;默认按汽油(B027)、半径 1000 米、返回 3~5 条简洁整理即可满足绝大多数「附近最便宜的加油站」类需求。更完整的 Skill 级应答规范(必问问题、字段整理模板、Done when 检查清单)可继续参阅 cheap-gas-nearby/instruction.md 与 cheap-gas-nearby/SKILL.md。
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考