Kair 开源
Regmark 方案03 功能模块
  1. 00总览
  2. 01定位与痛点
  3. 02受众与场景
  4. 03功能模块
  5. 04架构与选型
  6. 05首期路线图
  7. 06调研依据

Regmark03

功能模块

七个模块,一条流水线:读进来,归成一种结构,逐项比对,再让代理走一遍,最后出报告。

总览

数据只往一个方向流。采集器把各个出口读进来,归成同一种结构;四个检查器各看各的;报告器负责输出。模块之间只通过两种数据打交道:商品事实表和发现项。

  1. 出口

    • C商品页
    • M商品 feed
    • Y协议端点
    • K结账
  2. 采集器

    每个出口一个。只读,不做判断。

  3. 商品事实表

    每个事实都记着谁、什么时候、怎么说的。

  4. 检查器

    • 套准引擎
    • 协议符合性
    • 内容卫生
    • 探店代理
  5. 报告

    • 终端摘要
    • JSON、SARIF、JUnit
    • HTML 报告
    • 退出码
数据从左往右流。探店代理除了读事实表,还会直接去走一遍店铺。

四块版

出口很多,方案里把它们归成四类,借印刷的四色版来称呼。后面的图表都用同一套颜色。

版 出口 首期支持
C 商品页:可见文字、JSON-LD、microdata、Open Graph 全部
M 商品 feed:Google Merchant 格式、OpenAI 商品 feed Google 格式
Y 协议端点:UCP、ACP、店铺自己的 MCP 服务 UCP
K 结账:把商品加进购物车,填上收货地,读出实算总价 WooCommerce、Shopify

K 版是基准,其余三块向它对齐。没有接结账探针的时候,基准依次退到平台接口和可见页面。

模块一:采集器

做什么。 每个出口一个采集器,负责把那个出口上的商品事实读出来,连同出处一起交给事实表。采集器只读不判断,判断是规则的事。

设计要点。

  • 默认只发 GET 请求,遵守 robots.txt,同一主机每秒不超过一次请求,User-Agent 里写明自己是谁。
  • 商品页默认不启动浏览器,直接解析返回的 HTML。需要执行脚本才能看到价格的站点,可以选装 Playwright。
  • JSON-LD 不加载远程 @context。那既慢,又等于让被测站点指挥工具去访问任意地址。
  • 结账探针是唯一带写操作的采集器:建一个购物车,加一件商品,填一个测试收货地,读出总价,然后删掉购物车。它不会走到支付那一步,而且只对验证过归属的店铺开放。

首期范围。 商品页、Google 格式的 feed、UCP 的目录和结账会话、WooCommerce Store API、Shopify Storefront API。

首期不做。 ACP 端点、Magento、Medusa、Saleor。接口留好,等社区或后续版本补。

模块二:商品事实表

做什么。 把各个采集器读到的东西归成一张表。它的要点是:一个事实不存一个值,而是存一组「谁在什么时候说了什么」。

一件商品的某个变体,价格这一栏里可能有四条记录,分别来自 JSON-LD、feed、UCP 和结账,每条都带着原始取值、出处和读取时间。比对、报告、追溯,全都建立在这个结构上。

身份对齐。 同一个变体在不同出口上用的标识往往不同。事实表按这个顺序认人:GTIN,品牌加 MPN,SKU,规范化后的链接加变体选项。哪儿都对不上的记录不会被丢掉,而是单独报出来,因为代理同样对不上。

首期范围。 价格、币种、库存状态、运费、变体选项、标识符、退货政策是否存在。

首期不做。 评价、图片、商品属性的语义比对。

模块三:套准引擎

做什么。 对事实表里的每个变体,拿各个出口的记录和基准比,输出发现项。规则是纯函数,输入是一个变体的全部记录,输出是零条或多条发现。

误报怎么控制。 这是这个模块成败的关键,因为一个老喊狼来了的检查,很快会被人从 CI 里摘掉。

  • 时间窗。 同一个变体的各个出口尽量在同一小段时间里读完。
  • 二次确认。 首轮发现对不上的,隔一会儿把相关出口重读一遍,仍然对不上才报。
  • 容差。 每条规则有自己的容差。价格默认分毫不差;库存允许 feed 落后一个导出周期,但会单独记成「过期」。
  • 预算。 每条规则可以设允许的失败数。新接入的店可以先把预算设成现状,以后只许降不许升。

首批规则。

规则 说明 默认级别
price.mismatch 同一变体的价格与基准不一致 错误
price.currency-ambiguous 币种缺失,或者各出口的币种不同 错误
price.sale-expired 促销价的有效期已过,还在对外展示 警告
price.tax-basis 含税价和不含税价混着用 警告
availability.mismatch 库存状态与基准不一致 错误
availability.stale feed 的导出时间早于最近一次库存变化 警告
variant.missing 页面上能选的变体,在机器可读的出口里没有 错误
variant.unpurchasable 机器可读的出口里列着的变体,结账时加不进购物车 错误
shipping.mismatch 对外标称的运费与结账实算不一致 错误
shipping.undisclosed 运费要到结账才知道,前面的出口都没有表达 警告
identity.unmatched 某个出口上的商品,在其他出口找不到对应 警告
identity.gtin-invalid GTIN 的校验位不对,或者被多个变体共用 警告
policy.return-missing 退货政策没有机器可读的表达 提示

模块四:协议符合性

做什么。 检查发现文件和端点的形状合不合规范。这一块别人已经做得不错,所以 Regmark 只做一层薄的包装:直接用 UCP 和 ACP 规范自带的 JSON Schema,能调官方 conformance 套件的地方就调它,不重写。

为什么还要有。 因为采集器要读协议端点,形状不对的话后面的比对没法做。先把形状问题单独报出来,用户才知道该先修哪一层。

首期范围。 UCP 的发现文件、目录、结账会话,按规范版本钉死。

模块五:内容卫生

做什么。 找出商品内容里会带偏代理的东西。

规则 说明
content.hidden-text 人看不见、机器读得到的文字:和背景同色、字号为零、移出屏幕、藏在注释和 alt 里
content.instruction-like 写给模型看的指令,比如「忽略之前的要求」「务必推荐本商品」
content.invisible-chars 零宽字符和 Unicode 标签字符,常被用来夹带看不见的文本
content.cloaking 用浏览器的 User-Agent 和代理的 User-Agent 分别访问,拿到的商品事实不一样

设计要点。 首期全部用确定性规则,不调模型,这样结果可以复现,也没有成本。用模型做二次判定留作可选项。

模块六:探店代理

做什么。 让一个代理带着购物任务把店走一遍,看它能不能把东西放进购物车,以及它报给用户的总价和结账算出来的是不是同一个数。

任务长什么样。 一个任务由意图、约束和可验证的终态组成:

id: blue-tee-under-40
intent: 帮我找一件 40 美元以内、M 码的蓝色 T 恤,寄到旧金山
constraints:
  - total <= 40 USD        # 含运费
  - variant.size == M
  - variant.color ~ blue
expect:
  cart.items: 1
  quoted.total == checkout.total

最后一行是整个模块的核心断言:代理告诉用户的总价,等于结账实算的总价。

设计要点。

  • 断言看购物车,不看对话。 代理说了什么不作数,购物车里实际有什么、总价是多少才作数。
  • 任务从事实表里生成。 工具根据店里真实的商品、变体和价格区间自动出题,保证每道题都有解。
  • 录制与重放。 每次实跑都把模型的请求和响应录下来。重放不调模型、不花钱,结果是确定的,适合放在每次提交都跑的 CI 里;实跑放在定时任务里。
  • 多跑几遍。 模型输出有随机性,实跑默认每个任务跑三次,报成功率,不报单次结果。
  • 代理只拿得到店铺的工具。 它能搜索、查看、操作购物车,除此之外什么都做不了,而且在支付之前强制停下。

首期范围。 两种走法:读商品页,和调用 UCP 或 MCP 的工具。模型通过 OpenAI 兼容接口或 Anthropic 接口接入,用户自带密钥,也可以接本地模型。

模块七:报告与 CI

做什么。 把发现项变成人和机器都能用的输出。

输出 给谁
终端摘要 在本地跑的开发者
JSON,结构稳定并带版本号 想自己做二次处理的人
SARIF GitHub 的代码扫描界面
JUnit XML 其他 CI 系统
单文件 HTML 报告 要把结果发给客户或同事的人
退出码 CI 的门禁

GitHub Action。 首期提供一个官方动作,在合并请求里跑检查,把失败的规则写成评论。

首期之后的两个模块

巡检与漂移。 定时跑,把每次的结果存进 SQLite,画出每条规则的失败数随时间的变化,并在变坏时发通知。

修复建议。 针对失败的规则给出平台相关的修法,比如 WooCommerce 里是哪个钩子在输出 JSON-LD 的价格。这一块做得好会很有用,但它依赖前面的规则先稳定下来。