k-skill 实战:用 ev-subsidy-status 无头查询韩国地区电动汽车购车补贴余量
2026/9/18 6:38:00 网站建设 项目流程

k-skill 实战:用 ev-subsidy-status 无头查询韩国地区电动汽车购车补贴余量

【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill

韩国环境部(환경부)"无公害车综合网站"(무공해차 통합누리집)会公开各地区电动汽车购车补贴的申请、交付与剩余数量,但页面正文受pnp4web/penc脚本保护,且接口通常依赖人工操作。k-skill 项目中的ev-subsidy-status技能封装了同名 npm 包(源码位于 packages/ev-subsidy-status),在不登录、不开浏览器、不配置 API Key 的情况下,直接解析官方公开响应并恢复受保护的 HTML,从而查询特定地区、特定车种(电动乘用车/货车/客车)的补贴余量、公告状态与模型级换算金额。读完本文,你将掌握该技能的完整命令用法、pnp4web无代码执行的解码原理、公告状态判定优先级与全部失败模式,可直接用于本地查询或作为 Agent 技能接入。

技能定位与适用场景

ev-subsidy-status 在仓库中既是一个 k-skill(ev-subsidy-status/skill.json),也是一个可独立安装的 npm CLI 包。其定位非常明确:只读查询韩国地方自治团体发布的电动汽车购车补贴"民간공고(民间公告)、접수(受理)、출고(交付)、출고잔여(交付剩余)"数量与公告状态,不涉及申请、支付或任何写操作。

技能元数据(见 ev-subsidy-status/SKILL.md)将该技能归类为local-info,面向ko-KR用户,典型问题是"我家所在地区的电动乘用车补贴还剩多少、还能不能申请、某车型能拿多少补贴"。在 Dolshoi 等运行时中,它还会基于官方页面继续完成后续动作;在通用环境中则只负责查询与汇总。

核心卖点:基本查询无需浏览器、API 密钥、登录或代理。只有在官方页面结构变更需要诊断、或需要精确读取模型级补贴时,才可选择接入用户自行启动的 Aside Browser、BrowserOS 或 Chrome/Chromium CDP 会话。由于该公开页面本身是无密钥(keyless)公开接口,因此不经过k-skill-proxy代理转发,减少了链路复杂度。

前置条件与公共访问路径

运行环境要求

  • Node.js 18+(package.json 中engines.node明确声明>=18,因为代码使用全局fetchURLSearchParams);
  • ev-subsidy-statusnpm 包(仓库内版本为 0.2.0,通过npx ev-subsidy-status ...直接调用)。

官方公开端点

技能操作的两个官方页面:

  • 基础(全国)支付现状页(POST 提交查询参数):
https://ev.or.kr/nportal/buySupprt/initSubsidyPaymentCheckAction.do
  • 模型别补贴弹窗页(POST 提交查询参数):
POST /nportal/buySupprt/psPopupLocalCarModelPrice.do year=<year>&local_cd=<region-code>&car_type=<11|12|13>

这两个端点在源码 constants.js 中分别定义为STATUS_URLMODEL_SUBSIDY_PATHcar_type的取值范围11|12|13与三种车种一一对应:11=电动乘用车(승용),12=电动货车(화물),13=电动客车(승합)。

已确认的 DOM 结构

技能在开发时确认了官方页面的关键 DOM 选择器:

  • 市道(시도)下拉:#localDo_cd
  • 市郡区(시군구)下拉:#local_cd1
  • 结果表格列:시도(市道)、지역구분(地区区分)、차종구분(车种区分)、공고파일(公告文件)、접수방법(受理方式)、민간공고대수(民间公告数量)、접수대수(受理数量)、출고대수(交付数量)、출고잔여대수(交付剩余数量)、비고(备注)

这些选择器是技能解析表格、识别"包含출고잔여列的目标表格"的依据(见 http.js 中的extractStatusRows:遍历所有<table>,找到文本包含출고잔여的那张表)。

核心原理:pnp4web 保护页面的无代码执行解码

页面虽然无需登录即可访问,但正文被pnp4web/penc混淆脚本保护——直接抓取得到的只是"空壳"(shell HTML),真正的支付现状表格被打包进了一个受保护的 payload,需要通过官方下发的pnp4web.js字符表才能还原。

ev-subsidy-status 的默认传输路径(direct-http)在 pnp.js 中实现了完整的解码流程,其安全底线是:绝不执行远程 JavaScript(不使用eval/vm),只做字符替换级解析。具体步骤如下:

  1. 识别空壳响应:通过正则/meta[^>]+name=['"]penc['"]/i判断页面是否带penc元标记,只有带该标记才需要解码(无标记则直接返回 HTML)。
  2. 提取pnp4web.jsURL:用正则匹配<script name="pnp4web" src="...">,将相对路径基于STATUS_URL解析为绝对地址。
  3. 下载官方字符表源码:抓取pnp4web.js文本(带缓存cachedPnp,避免重复请求;当 URL 变化或传入自定义 fetch 时重新获取)。
  4. 解析字符表:先抽取var In=[...]数组中的字符串分片(逐一做\xHH/\uHHHH十六进制与 Unicode 转义还原),再按o0:...o6:...七个表达式从分片索引中拼出 7 组基础字母表;任一字母表长度不足 64 即判定UPSTREAM_DECODE_FAILED
  5. 还原受保护 payload:从空壳页面的onload="_0xac('...')"中提取加密串,其首字符为字母表索引、次字符为轮换量(rotation),用"轮换后的字母表"按 Base64 变体规则逐 4 字符解出 3 字节,最终Buffer.toString("utf8")得到真实 HTML。

从源码结构看,这种实现把"脚本保护"降维成"静态字符表解析",既绕过了对浏览器渲染的依赖,也避免了在 Node 进程里执行第三方混淆代码的安全风险。官方脚本或 payload 格式一旦变化,技能会以UPSTREAM_DECODE_FAILED(找不到字符表、空 payload、非法标识符等)明确报错,而不是静默返回错误数据。

解码完成后,HTTP 路径还做了一件事:检查解码后 HTML 中<option selected>的年份是否与请求年份一致,不一致则抛YEAR_NOT_AVAILABLE(http.js 的fetchDecodedStatusHtml),从源头杜绝"请求 2026 却拿到 2025 数据"的错位。

CLI 安装与命令总览

包通过bin字段暴露ev-subsidy-status命令(入口 cli.js),支持status(查询支付现状)与regions(搜索地区候选)两个子命令。完整参数如下:

参数别名说明
--region <지역>-r市道 + 市郡区,如경기 성남시(status 命令必填)
--query <검색어>-qregions 命令的地区搜索词
--vehicle <type>车种:passenger/승용cargo/화물bus/승합,默认passenger
--year <year>基准年份,默认 Asia/Seoul 当前年
--model <model>查询模型别补贴与剩余换算值
--transport <t>direct-http(默认)或browser
--provider <p>browser 传输时的auto/aside/browseros/chrome-cdp
--cdp-url <url>用户已启动的 Chrome/BrowserOS CDP 地址
--timeout <ms>页面等待超时(毫秒)
--json输出 JSON(默认输出人类可读文本)

命令行解析在parseArgs中实现,同时支持--vehicle/--vehicle-type两种写法;非 JSON 输出由formatStatus生成,其中数量格式化函数formatCount会区分우선(优先)/법인·기관(法人·机构)/택시(出租车)/중소기업(中小企业)/일반(一般)明细。

工作流一:地区与车种归一化

技能的第一步是规范化查询参数(对应原指令 Workflow 第 1 步):

  • 地区:尽量以시도 + 시군구形式输入,如경기 성남시
  • 重名歧义:像중구(首尔、大田、釜山、仁川都有)、강서구这类同名市郡区,先用regions命令列出候选,再让用户指定市道:
npx ev-subsidy-status regions --query 중구
  • 车种:默认passenger(전기승용),支持passenger/승용cargo/화물bus/승합三种。
  • 年份:默认取 Asia/Seoul 时区的当前年份。

地区与车种归一化在源码中有完整映射(constants.js):

  • VEHICLE_TYPES定义了三种车种及其carTypeCode(11/12/13)与别名表,resolveVehicleType对别名做小写归一后匹配,无法识别时抛出VEHICLE_TYPE_NOT_AVAILABLE
  • SIDO_ALIASES收录了 17 个市道的全部写法(如서울/서울시/서울특별시강원/강원도/강원특별자치도),canonicalSidoFromQuery用它把用户输入收敛为标准市道名;
  • localMatches支持去掉시/군/구后缀的模糊匹配(如성남可命中성남시),但必须short.length >= 2防止过度匹配。

地区解析最终落到resolveRegionFromRows(http.js):在全表行中按市道 + 市郡区双重过滤,去重后若为 0 行抛REGION_NOT_FOUND,多于 1 个抛REGION_AMBIGUOUS(并在details.candidates中给出全部候选,方便用户二次选择),恰好 1 个才确定地区代码。local_code从公告下载函数goDownloadFile(...)的第二个参数中读取,市道代码则由其前两位拼00得出。

工作流二:查询官方支付现状

确定地区后,执行 status 查询:

npx ev-subsidy-status status \ --region "경기 성남시" \ --vehicle passenger \ --year 2026 \ --json

该命令内部(getSubsidyStatusHttp)会向initSubsidyPaymentCheckAction.doPOST 提交car_typeyear1localDo_cd=alllocal_cd1=all(请求全国行,再在客户端过滤目标地区)。正常结果中transport应为direct-http,说明走的是无需浏览器的默认路径。

关键返回字段

字段含义
status.notice_count民间公告数量
status.application_count受理(申请)数量
status.delivered_count交付数量
status.delivery_remaining_count交付剩余数量
availability.label公告状态判定(open/scheduled/closed/unknown 等)
status.note地方自治团体备注原文
source.fetched_atKST 时区查询时间

数值单元格以전체(总计) / 우선순위(优先) / 법인·기관(法人·机构) / 예약 대상군(预约对象群) / 일반(一般)五档保留:乘用车的预约对象群是taxi(出租车),货车的对应别名为small_business(中小企业),CLI 文本输出中会按车种动态插入相应明细标签。

值得注意的鲁棒性处理:如果目标群体(대상군)数值为负数、或整体与部分之和不等,技能不会擅自将值"修正"为 0,而是保留原始数字并附加警告返回,把异常如实暴露给上层(原指令 Failure modes 结尾明确这一点)。

工作流三:备注优先于数字的公告状态判定

官方页面只给数量,不给明确的"还能不能申请"结论,因此技能实现了备注文本优先的状态判定(availability.js 的classifyAvailability),判定优先级如下:

  1. 备注含마감(截止)/소진(售罄)/접수 종료(受理结束)/신청 종료(申请结束)closed
  2. 备注含접수 예정(受理预定)/추경 예정(追加预算预定)/추가 공고 예정(追加公告预定)/재공고 예정(重新公告预定)scheduled
  3. 备注含접수 중(受理中)/신청 기간(申请期间)/접수 기간(受理期间)/신청 가능(可申请)open
  4. 无上述明示语、但剩余数量为正数 →unknown_with_remaining_count
  5. 无任何判定依据 →unknown

classifyAvailability同时输出basis(判定依据,如note:closedremaining_count:positive)与warnings。一个关键规则是:即便剩余数量为正,只要备注明确写了"截止/售罄",就不得声称可申请——此时会生成警告 "출고잔여대수는 양수지만 지자체 비고에는 마감 또는 소진으로 표시됩니다."(剩余数量虽为正数,但备注标注截止或售罄),并仍以closed为准。

工作流四:模型指定时的补贴与换算

官方页面只提供"数量",不提供"金额"。当用户关心具体车型能拿多少钱时,可通过--model触发模型级查询:

npx ev-subsidy-status status \ --region "서울 강남구" \ --vehicle passenger \ --model "모델명" \ --json

即使走直接 HTTP 路径,技能也会用同一个年份、地区代码与车种调用psPopupLocalCarModelPrice.do,解析模型表(http.js 的extractModelSubsidySnapshot会筛选表头同时包含제조사(制造商)모델(型号)국비(国费)的表格,并取行数最多的候选),逐行解析制造商、模型、国费、地方费与合计。

匹配处理有三条分支(parseModelSubsidyRows+attachModelEstimate):

  • 匹配到多个细分车型:不擅自任选其一,而是在model_subsidy_candidates中返回全部候选及各自的 1 台补贴与剩余换算值,并追加警告提示"按配置(trim)区分确认";
  • 恰好匹配一个细分车型:额外返回model_subsidyremaining_budget.model_equivalent_estimate_krw
  • 匹配不到:保留地区级数量结果,设置model_lookup_error并输出警告,模型查询失败不影响地区数量结果

换算值的含义与边界

remaining_budget.model_equivalent_estimate_krw的计算假设是:

官方交付剩余数量 × 所选模型的(国费+地方费)

其实现位于 estimate.js 的estimateModelEquivalent:当剩余数量或单台补贴额不是有限正数时,返回exact_available: falseexact_amount_krw: null的"不可用预算"结构;否则用remainingCount * subsidyPerVehicleKrw取整,并在estimate_assumptions中写明三条假设(全部剩余数都分配给所选模型、按确认的国费+地方费计算、不含按购买者特征的追加支持与对象群间数量转换)。

因此该换算值不是地方自治团体的精确预算余额:模型别金额、购买者追加支持、数量转换、受理后的预约/取消都会使其偏离实际可用预算。回答用户时必须明确这是"换算值/估算"而非"精确可用金额",这也是下一条工作流的基本原则。

工作流五:保守作答的输出规范

最终答复必须包含(原指令 Workflow 第 5 步):

  • 地区、车种、基准年份;
  • 民间公告/受理/交付/交付剩余四项数量;
  • 公告状态与备注核心语句;
  • 若有模型换算值,说明计算假设;
  • 固定警告语:"출고잔여대수는 실제 신청 가능 대수 및 정확한 원화 잔액과 다를 수 있음"(交付剩余数量可能与实际可申请数量及精确韩元余额不同);
  • 官方来源 URL 与 KST 查询时间。

CLI 的formatStatus默认输出已经内置了以上要素,包括末尾的注意语与출처: <source.url>行;--json模式下则由调用方按相同规则组织。在exact_available=false的场景,必须同时给出原因,不能把估算伪装成事实。

可选浏览器路径:诊断与模型查询的兜底

默认direct-http无需浏览器,但在两种情况下可显式切换到浏览器传输:官方页面结构变更需要诊断,或需要更可靠地读取模型别换算值。命令示例:

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

浏览器路径的行为约束(原指令 Optional browser behavior)非常严格:

  • domcontentloaded事件后,显式等待目标选择器与地区行出现;
  • 不使用networkidle等待策略——因为pnp4web有后台请求,networkidle 会导致永久等待;
  • 通过渲染后的<option>的 label/value动态解析地区代码;
  • 不关闭用户既有的标签页与配置文件(profile);
  • 只清理技能自己创建的页面/上下文,并仅对受支持的 client 执行 disconnect;
  • 绝不绕过登录、CAPTCHA、支付、电子签名、最终提交等边界。

浏览器接入基于k-skill-browser-runtime(见 package.json 的依赖声明),连接用户已启动的 Aside Browser、BrowserOS 或 Chrome CDP 会话;若没有可连接的浏览器,则报BROWSER_UNAVAILABLE。错误包装逻辑(errors.js 的wrapBrowserError)会把上游CAPTCHA_DETECTEDAUTH_REQUIREDUNKNOWN_PROVIDERUNAVAILABLE/PLAYWRIGHT_UNAVAILABLE分别映射为技能级错误码,其余统一归为UPSTREAM_FAILED,并保留upstream_code便于排查。

完成标准(Done when)

一次成功的查询应满足:

  • 请求的地区与车种与官方表格中的行一致;
  • 返回公告/受理/交付/交付剩余数值与地方自治团体备注;
  • 显式截止语句优先于正数剩余数量;
  • 无法确知精确韩元预算余额时,返回exact_available=false并说明原因;
  • 展示来源 URL 与 KST 查询时间。

失败模式全表

技能以稳定错误码暴露全部异常(原指令 Failure modes,与 errors.js 及 http.js 的实现一致):

错误码触发条件
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_FAILEDpnp4web字符表或保护 payload 格式变化
MODEL_LOOKUP_FAILED模型别补贴表读取失败
UPSTREAM_TIMEOUT官方站点响应超时(默认超时 30000ms,见 http.js)

CLI 出错时以 JSON 输出{ error: { code, message, details } }formatError),便于 Agent 或上层脚本结构化处理;EvSubsidyError实例携带codemessagedetails与可选cause

测试与质量保障

该技能在仓库中配有完整测试:node --test驱动 test/index.test.js、test/http.test.js、test/browser.test.js,覆盖状态聚合、HTTP 解码链路与浏览器行为;lint脚本对所有源码与测试文件做node --check语法校验。感兴趣的可直接运行:

cd packages/ev-subsidy-status && npm test

总而言之,ev-subsidy-status 的价值在于把"被脚本保护的官方公开数据"安全、可重复、无浏览器地转化为结构化 JSON:先归一化地区与车种,再以备注优先判定公告状态,最后在需要时给出带假设说明的模型换算值——整个过程对上游格式变化以明确错误码快速暴露,保证了查询结果的可审计性与数据真实性。

【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询