ev-subsidy-status:基于环境部无公害汽车综合网站的无登录韩国电动车补贴查询方案
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
本指南深入解析 k-skill 仓库中ev-subsidy-status包(对应根目录ev-subsidy-statusSkill)的实现与使用:它如何在不登录、不依赖用户浏览器、不使用代理与 API Key 的前提下,直接读取韩国环境部无公害汽车综合网站(ev.or.kr)公开的电动车购买补贴发放现状(公告、受理、出库、出库剩余台数),并支持按车型与细分车型查询国费、地方费补贴。读完本文,你将掌握该工具的全部 CLI 命令与参数、direct-http与browser双传输机制、pnp4web保护 HTML 的还原原理、返回数据字段语义、状态判定规则以及错误码体系,可直接复现官方文档示例并在 Agent 场景中正确消费其结果。
一、工具定位:面向韩国地方自治团体的电动车补贴查询
ev-subsidy-status(包目录)是一个以 Node.js 18+ 运行的命令行工具与库,其核心目标是用一行命令回答"我所在地区还能不能申请电动车补贴、还剩多少台"这类问题。它的特点与边界在官方 README.md 中明确界定:
- 查询环境部无公害汽车综合网站公开的购买补贴发放现状,全程无需登录,也不使用用户浏览器;
- 默认传输方式为
direct-http:从公开响应中读取官方pnp4web.js字符表,在不执行远程代码的前提下还原被保护的 HTML; - 不使用代理,也不使用 API Key;
- 官方页面结构一旦变化,可通过
--transport browser显式选择诊断用浏览器路径; - 官方页面提供的是公告、受理、出库、出库剩余台数,而非韩元余额;仅在指定
--model时,才在 direct-http 路径下额外查询模型级国费、地方费与合计,并给出剩余换算值; - 输入名称匹配到多个细分模型时,返回全部候选,绝不任意挑选其中一个;换算值不是精确的可支配预算余额。
在仓库中它同时以 Skill 形态存在(ev-subsidy-status/SKILL.md),供 Agent 在回答用户所在地区电动车补贴问题时调用,并在无公害汽车综合网站页面进行后续动作时使用官方表层界面。
二、快速开始:安装与三个核心命令
包声明在 package.json 中,bin入口为src/cli.js,main为src/index.js,运行时要求 Node.js >= 18,运行时依赖仅有k-skill-browser-runtime(browser 路径使用)。
官方 README 给出的三条基础命令可直接运行(通过npx免全局安装):
# 查询某地区某车型某年的补贴发放现状 npx ev-subsidy-status status --region "경기 성남시" --vehicle passenger --year 2026 # 查询某地区指定模型的补贴并输出 JSON npx ev-subsidy-status status --region "서울 강남구" --model "모델명" --json # 按关键词搜索地区(解决同名区划歧义) npx ev-subsidy-status regions --query "중구"第一条命令会输出类似下方的人类可读结果(格式来自 cli.js 的 formatStatus):
경기 성남시 전기승용 보조금 현황 조회: 2026-07-18T20:00:00+09:00 민간공고대수: 1,949대 (우선 55, 법인·기관 0, 택시 20, 일반 1,043) 접수대수: ... 출고대수: ... 출고잔여대수: ... 상태: closed 접수방법: ... 비고: ...输出末尾会强制附上免责声明:"출고잔여대수는 실제 신청 가능 대수와 다를 수 있으며 정확한 원화 예산 잔액이 아닙니다."(出库剩余台数可能与实际可申请台数不同,也不是精确韩元预算余额),以及官方来源 URL 与 KST 查询时刻——这正是源码中"保守回答"设计原则的体现。
三、命令与参数全解
CLI 参数解析实现在 cli.js 的 parseArgs 与 printHelp 中。工具提供两个子命令:
| 子命令 | 说明 |
|---|---|
status | 查询指定地区的补贴发放现状(默认命令,可省略) |
regions | 按查询词搜索地区候选,返回sido_name/local_code等字段 |
3.1 全局参数
| 参数 | 别名 | 类型 | 说明 | 默认值 |
|---|---|---|---|---|
--region <region> | -r | string | 查询地区,建议"시도 + 시군구"格式,如경기 성남시 | 无(status 必填) |
--query <text> | -q | string | regions命令的地区搜索词 | 无(regions 必填) |
--vehicle <type> | --vehicle-type | string | 车型:passenger/승용、cargo/화물、bus/승합 | passenger |
--year <year> | - | number | 基准年度,默认 Asia/Seoul 当前年 | 当前年 |
--category <category> | - | string | 类别过滤,默认all | all |
--model <model> | - | string | 指定模型后查询地方自治团体的模型级补贴与剩余换算值 | 无 |
--transport <transport> | - | string | direct-http(默认)或browser | direct-http |
--provider <provider> | - | string | browser 传输下的浏览器供应方:auto、aside、browseros、chrome-cdp | 依运行时 |
--cdp-url <url> | - | string | 用户自行启动的 Chrome/BrowserOS CDP 地址 | 无 |
--timeout <ms> | - | number | 页面等待超时(毫秒),源码默认 30000ms | 30000 |
--json | - | flag | 输出 JSON 而非人类可读文本 | 关 |
regions命令还支持位置参数:npx ev-subsidy-status regions "중구",与--query "중구"等价(见 cli.js parseArgs)。
3.2 车型别名的归一化
车型解析位于 constants.js resolveVehicleType,支持韩英双语别名,不支持的车型会抛出VEHICLE_TYPE_NOT_AVAILABLE错误:
| key | 页面标签 | carTypeCode | 支持别名 |
|---|---|---|---|
passenger | 전기승용(电动乘用) | 11 | passenger、car、승용、전기승용、전기차 |
cargo | 전기화물(电动货车) | 12 | cargo、truck、화물、전기화물 |
bus | 전기승합(电动客车) | 13 | bus、van、승합、전기승합 |
3.3 地区名的归一化与歧义处理
地区输入会经过 parse.js normalizeRegionKey 做归一化:去除空白、将경기도→경기、충청북도→충북、제주특별자치도→제주等全称折叠为标准名,并剥离특별시/광역시/특별자치시/도等后缀。constants.js的SIDO_ALIASES(constants.js)内置 17 个市道(首尔、釜山、大邱、仁川、光州、大田、蔚山、世宗、京畿、江原、忠北、忠南、全北、全南、庆北、庆南、济州)的全部常用写法。
对于중구、강서구这类多个市道共有的区名,系统会抛出REGION_AMBIGUOUS,此时应先用regions找到候选并让用户指定市道:
npx ev-subsidy-status regions --query "중구"四、双传输架构:direct-http 与 browser
传输选择逻辑位于 index.js browserRequested:当显式指定transport: "browser",或传入page、runtime、provider、cdpUrl任一选项时走浏览器路径,否则走直连 HTTP 路径。
4.1 direct-http:解析 pnp4web 字符表,不执行远程代码
这是默认且推荐的路径,instruction.md 对其行为做了明确描述,源码在 http.js 与 pnp.js:
- 向
https://ev.or.kr/nportal/buySupprt/initSubsidyPaymentCheckAction.do发送POST,表单字段为car_type(车型码)、year1(年度)以及全国地区条件localDo_cd=all、local_cd1=all(http.js fetchDecodedStatusHtml); - 若响应含
<meta name="penc">标记,说明正文被pnp4web保护,则从 HTML 中提取官方pnp4web.js的 URL(pnp.js extractPnpScriptUrl); - 只解析脚本中声明的
var In=[...]片段字符表与o0~o6七个字母表(parsePnpAlphabets),绝不通过eval/vm执行远程代码; - 从页面
onload中提取受保护 payload,按其首字符选择字符表、次字符做轮转位移,再进行类 Base64 解码还原出 UTF-8 HTML(decodePnpPayload); - 在还原后的表格中定位含"출고잔여"的表(extractStatusRows),解析十列官方行,并从公告下载函数
goDownloadFile(...)的参数中读取地区代码; - 若响应不含
penc标记(页面未被保护),则直接作为明文处理。
请求带默认请求头与 30 秒超时(http.js DEFAULT_TIMEOUT_MS),超时会转为UPSTREAM_TIMEOUT。该路径全程无浏览器、无登录、无代理、无 API Key,是 README 所述"即使没有用户浏览器也应正常工作"的默认路径。注意:官方公开的是 keyless 页面,因此本工具不使用k-skill-proxy。
4.2 browser:诊断用浏览器路径
当官方页面结构变更、需要诊断渲染结果或核对模型级换算时,可显式选择浏览器路径:
npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider auto npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider aside npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider browseros npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider chrome-cdp该路径通过k-skill-browser-runtime(package.json 依赖^0.4.0)连接用户自己启动的 Aside Browser、BrowserOS 或 Chrome CDP 会话,相关实现见 browser.js withAutomationPage。其设计原则在 instruction.md 的 "Optional browser behavior" 中有明确约束:
- 页面加载使用
domcontentloaded,刻意不用networkidle,因为pnp4web存在后台请求会导致永不静默; - 通过轮询显式等待目标 selector 与地区行出现(waitFor 默认 300ms 轮询间隔);
- 用渲染后
<option>的 label/value 动态解析地区代码(resolveRegion); - 不关闭用户已有的标签页与个人资料,只清理本 Skill 创建的页面/上下文,并仅对支持的 client 执行
disconnect; - 绝不绕过登录、CAPTCHA、支付、电子签名与最终提交等边界——检测到 CAPTCHA 立即抛
CAPTCHA_DETECTED并停止(assertPublicStatusPage)。
浏览器上下文固定使用ko-KR语言与Asia/Seoul时区、1280×900 视口(browser.js withAutomationPage),保证页面渲染与解析结果一致。
五、返回数据结构与字段语义
status命令的 JSON 结果由 parse.js buildStatusResult 组装,顶层结构如下:
{ "query": { "region": "경기 성남시", "year": 2026, "vehicle_type": "passenger", "category": "all", "model": null }, "region": { "sido_name": "경기", "sido_code": "41000", "local_name": "성남시", "local_code": "41130" }, "status": { "sido_name": "경기", "local_name": "성남시", "vehicle_label": "전기승용", "notice_files": [{ "label": "본공고 1", "title": "", "onclick": "goDownloadFile('...')" }], "application_method": "출고등록순", "notice_count": { "total": 1949, "priority": 55, "corporate": 0, "reserved": 20, "general": 1043 }, "application_count": { "total": 1745, "priority": 550, "corporate": 49, "reserved": 60, "general": 1086 }, "delivered_count": { "total": 1740, "priority": 549, "corporate": 49, "reserved": 60, "general": 1082 }, "delivery_remaining_count": { "total": 209, "priority": 0, "corporate": 0, "reserved": 0, "general": 867 }, "note": "★ 공고 마감 ★ 추경예산 확보 후 재공고 예정", "warnings": [] }, "availability": { "label": "closed", "basis": ["note:closed"], "warnings": [], "official_remaining_count": 209, "pending_application_count": 5, "actual_application_count_known": false }, "remaining_budget": { "exact_available": false, "exact_amount_krw": null, "reason": "...", "model_equivalent_estimate_krw": null, "estimate_assumptions": [] }, "source": { "name": "환경부 무공해차 통합누리집", "url": "https://ev.or.kr/...", "fetched_at": "2026-07-18T20:00:00+09:00" }, "transport": "direct-http", "warnings": [] }关键语义说明:
- 四个台数对象均保留"전체(total)/ 우선(priority)/ 법인·기관(corporate)/ 예약 대상군(reserved)/ 일반(general)"五个槽位;乘用车的
reserved同时以taxi别名返回,货车的以small_business别名返回(parse.js categoryCountsForVehicle); source.fetched_at是 KST 格式的查询时刻(formatKst,UTC+9);pending_application_count= 受理台数 - 出库台数,表示在办申请量;actual_application_count_known: false表明公开数据无法给出精确的"实际可申请台数";remaining_budget.exact_available恒为false,因为公开发放现状无法精确计算韩元预算余额;- 当目标群数值出现负数、或分项之和与总数不一致时,不做修正,原样返回并追加 warning(countWarnings)。
六、状态判定:非注明优先于剩余台数
availability.label的判定逻辑在 availability.js classifyAvailability,优先级如下(instruction.md 亦有说明):
| 优先级 | 判定依据(비고 原文正则) | label |
|---|---|---|
| 1 | 含마감/소진/접수 종료/신청 종료 | closed |
| 2 | 含접수 예정/추경 예정/추가 공고 예정/재공고 예정 | scheduled |
| 3 | 含접수 중/신청 기간/접수 기간/신청 가능 | open |
| 4 | 无明确措辞但剩余台数为正 | unknown_with_remaining_count |
| 5 | 无任何依据 | unknown |
两条铁律:
- 备注原文优先于数字:即使剩余台数为正,只要备注写明"마감/소진",就判定为
closed并附 warning"出库剩余台数为正但备注为截止/耗尽"(availability.js)。测试 index.test.js "availability gives explicit closure text precedence" 验证了"일반 접수 마감, 추경 공고 예정"在剩余 20 台时仍判为closed。 - 不编造可申请性:无明确措辞时绝不擅自说"可申请",只报
unknown_with_remaining_count。
七、模型级补贴与剩余换算值(model 查询)
指定--model后,即便在 direct-http 路径下也会以POST请求官方模型补贴弹窗面https://ev.or.kr/nportal/buySupprt/psPopupLocalCarModelPrice.do,表单为year=<年度>&local_cd=<地区代码>&car_type=<11|12|13>(见 constants.js MODEL_SUBSIDY_PATH 与 http.js fetchDecodedModelHtml)。
模型表解析(parse.js parseModelSubsidyRows)会读取"제조사/모델/국비/지방비/합계"等表头并按输入名做模糊匹配,金额解析(parseMoneyKrw)自动识别억원、만원单位——例如1,200만원解析为 12,000,000 韩元(测试见 index.test.js)。
行为细节:
- 输入名命中多个细分模型时,返回
model_subsidy_candidates全部候选及各自换算值,不任意挑选,并追加警告提示用户区分 trim 级补贴(http.js getSubsidyStatusHttp); - 恰好命中唯一细分模型时,额外返回
model_subsidy与remaining_budget.model_equivalent_estimate_krw; - 模型查询失败不影响地区级台数结果,只附加
model_lookup_error与警告。
model_equivalent_estimate_krw的计算在 estimate.js estimateModelEquivalent:
공식 출고잔여대수(官方出库剩余台数)× 선택 모델의 국비+지방비(所选模型国费+地方费)其附带的三条假设(estimate_assumptions):剩余台数全部划拨给所选模型、按已确认的单台国费+地方费计算、不反映购买者特性追加支持与目标群间数量转换。官方 README 与源码均强调:这不是地方自治团体的精确预算余额,切勿向用户表述为"精确可用预算"。测试 index.test.js "model equivalent is clearly separate from exact budget" 专门断言exact_available=false、exact_amount_krw=null与换算值并存的边界。
八、错误码体系与失败模式
错误构造与包装位于 errors.js,CLI 失败时以 JSON 输出{ error: { code, message, details } }(cli.js formatError)。完整错误码清单(instruction.md "Failure modes"):
| 错误码 | 触发场景 |
|---|---|
REGION_REQUIRED | 未输入查询地区 |
REGION_AMBIGUOUS | 同名区划存在于多个市道,需连同市道一起输入 |
REGION_NOT_FOUND | 官方地区列表中找不到该地区 |
YEAR_NOT_AVAILABLE | 请求年度不在页面可选列表 |
VEHICLE_TYPE_NOT_AVAILABLE | 车型不受支持或页面无此车型项 |
BROWSER_UNAVAILABLE | 没有可连接的用户浏览器 |
UPSTREAM_BLOCKED | 空壳响应、被拦截或异常页面 |
CAPTCHA_DETECTED | 出现 CAPTCHA,工具不绕过 |
AUTH_REQUIRED | 公开页面变为登录流程,工具不绕过 |
RESULT_EMPTY | 目标地区无结果行 |
DOM_CHANGED | 官方页面选择器或表格结构变化 |
UPSTREAM_DECODE_FAILED | pnp4web字符表或保护 payload 格式变化 |
MODEL_LOOKUP_FAILED | 模型补贴表读取失败 |
UPSTREAM_TIMEOUT | 官方站点响应超时 |
其中UPSTREAM_DECODE_FAILED与DOM_CHANGED是官方脚本或表格结构变化时的"哨兵错误",指示需要切换到 browser 路径诊断(详见 http.js、pnp.js)。
九、源码结构地图
若需深入阅读,仓库中与本文相关的核心文件路径如下:
- 包根:
packages/ev-subsidy-status/package.json、packages/ev-subsidy-status/README.md - CLI 层:cli.js(参数解析、人类可读/JSON 输出、错误格式化)
- 入口分发:index.js(根据选项选择传输路径)
- 直连 HTTP 路径:http.js(表单提交、地区解析、模型表抓取)
- pnp4web 解码器:pnp.js(字符表解析与 payload 还原)
- 浏览器路径:browser.js(DOM 求值操作、地区/车型联动选择、与 k-skill-browser-runtime 的连接)
- 常量与别名:constants.js(URL、车型定义、市道别名)
- 解析与装配:parse.js、availability.js、estimate.js
- 测试:test/index.test.js(数值解析、状态判定、金额单位、CLI 参数、输出披露共 11 个用例)
- Skill 侧:
ev-subsidy-status/SKILL.md(Skill 元数据与硬性规则)、ev-subsidy-status/instruction.md(Agent 操作指引,与本文各节一一对应)
十、在 Agent 场景中的使用建议
综合官方 README、instruction.md 与源码行为,将ev-subsidy-status用于 Agent 或自动化流程时应注意:
- 默认走 direct-http:优先使用
--transport direct-http(默认),确认结果中transport为direct-http,确保在无浏览器环境可用; - 地区输入规范:尽量传"市道 + 市郡区",重名区划先用
regions消歧; - 解读优先级:先看
availability.label与status.note原文,再看台数;备注为closed时即使剩余为正也不要说"可申请"; - 保守披露:始终附带"出库剩余台数≠实际可申请台数≠精确韩元余额"的警示,并给出官方来源 URL 与 KST 查询时刻;
- 模型换算有假设:引用
remaining_budget.model_equivalent_estimate_krw时说明计算假设,不表述为精确预算余额;多候选时把model_subsidy_candidates全部列出让用户选择; - 不绕过边界:遇到
CAPTCHA_DETECTED、AUTH_REQUIRED时停止并如实上报,遵守 Skill 中"不绕过登录、验证码、支付、电子签名边界"的硬性规则; - 结构变化时切换诊断:出现
DOM_CHANGED或UPSTREAM_DECODE_FAILED时,用--transport browser --provider aside|browseros|chrome-cdp连接用户浏览器核验官方页面现状。
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考