d1-cli — 从命令行操作 Tiendas D1 是做什么的?
D1 是哥伦比亚最大的硬折扣连锁超市。其线上门店运行在 VTEX IO 上(账号 d1tiendas),这意味着网站的公开 API 就是集成界面——无需爬虫,无需管理员凭据。
调用
cd skills/commerce/d1-cli
# 1. 首先解析配送地点——其他所有操作都依赖于它
bun run src/cli.ts region --lat 4.6486 --lng=-74.0628
# 2. 然后搜索:价格和库存现在基于您的门店,而非全国
bun run src/cli.ts search leche entera --count 5
bun run src/cli.ts search --facets category-1/lacteos-y-huevos --sort price:asc
# 3. 在不修改购物车的情况下为购物篮定价
bun run src/cli.ts quote 262:2 892:1
# 3b. 或者让预算决定购物内容——按条件而非SKU
bun run src/cli.ts basket --budget 30000 arroz leche aceite huevos
# 4. 或者创建一个真实的购物车,然后交由人工支付
bun run src/cli.ts cart add 262 --qty 2
bun run src/cli.ts cart deliver-to
bun run src/cli.ts cart checkout # 输出URL;不执行支付
在任何命令后添加 --json 可获取机器可读的输出。退出码是约定,以下列表是唯一副本:
| 代码 | 含义 | 是否重试? |
|---|---|---|
0 | 操作成功 | — |
1 | D1 拒绝或无法访问——无法配送的地点、不可用或未知的SKU、服务中断 | 是,可能有帮助 |
2 | 命令调用错误 | 重试无意义 |
3 | 操作成功,但结果为空(substitute、basket) | 重试无意义 |
无法区分这些的智能体要么永远重试一个输入错误,要么在暂时性故障时放弃。3 存在是因为空类别既不是错误也不是正常结果。
注意 cart add 是设置该商品数量为 --qty,而不是增加。这是 D1 自身的语义,已通过实际测试验证。
导致简单 D1 自动化出错的两件事
库存是区域化的。 不指定区域查询商品目录,D1 会从全国目录返回结果,显示 10,000 件库存,卖家为虚拟的 1。将同一个SKU放到真实地址的结账模拟中,结果返回 withoutStock,因为实际负责配送的实体店没有该商品。始终先使用 d1 region;当显示全国价格时,CLI 会以纯文本形式发出警告。
解析区域也是 API 中最难以猜测的部分:坐标必须按 geoCoordinates={lon};{lat} 的格式传入——分号分隔,经度在前。使用逗号(VTEX 自己的 JSON 体中使用的同一对格式)会被拒绝并返回 CHK0119("地址必须包含邮政编码或地理坐标")——这条消息看起来像是参数缺失而非格式错误。test/region.test.ts 测试文件固定了此行为,防止后续清理时被意外修改。
价格以两种单位返回。 对于 SKU 262(Leche Entera Latti 900 ml,货架价 COP 3,500):
| API | 字段 | 值 | 单位 |
|---|---|---|---|
| intelligent-search | Price | 3500 | 整数比索 |
| catalog_system | Price | 3500.0 | 整数比索 |
| checkout orderForm | sellingPrice | 350000 | 百分之一比索 |
两者均未标注其单位。catalog.ts 在边界处将所有价格统一转换为百分之一,因此其余代码只使用一种单位,并且测试断言搜索来源的价格与结账来源的价格显示完全相同。
它不会做什么
d1 构建并定价购物篮。但不会支付。d1 cart checkout 输出一个 URL,人工审核总金额并完成支付。
该边界在 src/endpoints.ts 的一个列表中被两个地方检查:运行时,D1Client.request 在任何请求离开进程之前,如果解析后的 URL 不是 D1 自己的源且不属于 18 个已批准的路径之一,则会拒绝该请求;静态检查,test/safety.test.ts 会测试 src/ 下任何 /api/ 字面量是否未获得批准。添加端点(包括支付或其他)会失败,直到将其列入白名单,因此必须做出明确决策,而非默认生效。
运行时的检查是关键的,因为在审查后发现仅凭静态扫描是不够的:--facets '../../../..' 可以从搜索端点解析到订单结算,而所有源字面量仍然保持未批准状态。
准确地说,因为早期版本对此描述过于绝对:任何拥有写入权限的提交者都可以编辑白名单。仓库内的测试无法阻止这一点。
它存储的唯一凭证是店铺会话令牌——与登录后的浏览器持有的相同,范围限定为其所有者自己的订单和购物车——写入 ~/.config/d1-cli/session.json,权限为 0600。没有在任何地方读取 VTEX 管理员的 appKey/appToken。登录通过一次性邮件验证码进行;密码认证故意未实现。
结账关卡——始终以此结束
每次购物篮都应以 d1 cart checkout 结束,并根据其退出码采取行动。 这不是形式:它是唯一重新检查整个购物车与 D1 当前状态的命令,而购物车会在其他命令无法察觉的情况下过期。
0 所有商品均可配送——URL 可以安全交给用户
1 至少有一个商品无法配送;URL 已输出但购物车已损坏
2 命令调用错误
此行为的存在是因为在真实购物篮中每种情况都发生过:
- 某个商品一夜之间售罄。
PAN ARTESANAL INTEGRAL在添加后几小时变为no tiene inventario。购物车仍然显示,仍然有总金额——一个悄悄少了 6.490 比索的总金额,因为失效的商品已从总额中剔除。 - 地址更改导致所有商品无法配送。 九个商品返回
cannotBeDelivered,而每个商品仍携带有效的 SLA,购物车仍显示可支付的总金额。 - 配送费被低估了 12 倍,因为按商品分配的配送费看起来像是总金额。
在所有三种情况下购物车看起来正常。会话早期获取的报价并不能代表购物车当前的状态,因此永远不要交出在最后一次修改之前获得的结账 URL——重新运行关卡。
当返回 1 时,--json 会提供 undeliverable[] 数组,列出无法配送的商品行,以及 readyToCheckout: false。删除或替换这些商品行,然后重新运行。
替换 D1 无法供应的商品
d1 substitute 192 --limit 5 # 从相同类别中排名有库存的商品
退出码 0 表示有可推荐的商品;3 表示没有——这是使用 3 的两个命令之一,另一个是 d1 basket。
使用 3 而非 1,这一区分是关键:1 在此 CLI 的其他地方均表示 "D1 拒绝或无法访问",并建议重试。空类别不会因重试而变成非空,因此采用重试-1 策略的智能体会永远循环下去。退出码 0 返回空列表是另一个错误答案——它看起来像成功,会被当作有效结果处理。
每个推荐商品都列出了变更内容:品牌、包装规格、每公斤/升价格、整包价格,以及任何新增或移除的 Ley 2120 警告标签。这是输出内容,而非评分。同样的 900 ml 规格、同样标签的商品降价 12% 与增加了 "Exceso en Azúcares" 警告标签的商品是不同的,只有差异才能区分它们。
推荐而非直接替换。 substitute 从不修改购物车——它输出 d1 cart add 命令供人工批准。这并非出于谨慎而谨慎:用户两次明确表示宁可删除商品也不要错误的替换,且两次都是正确的。呈现候选商品及其权衡;由用户决定,并愿意删除该商品。
三个值得关注的信息:
- "该商品在您门店仍有库存。" 库存按门店划分,因此在其他地方缺货的商品在此处可能正常。在替换前先检查。如果没有提供
--lat/--lng,它会直接说明而不是猜测——因为没有门店可以做出按门店的声明。 - "扩展到第 2 级(共 3 级)" ——最细粒度的类别中没有有库存的商品,因此推荐来自更宽泛的分类,相应地更加宽松。
- "仅比较了 N 个商品" ——该类别大于一页。增加
--count参数。这在返回空结果时也会输出,因为 "此处无任何商品有库存" 的断言基于部分扫描,更需要这一说明。
当需求模糊时查找商品
对于 "我们需要大米" 而非 "购买 SKU 1092",有两个维度很重要,但都不是默认选项:
按单价而非总价排序。 --sort per-unit 使用哥伦比亚法律要求 D1 发布的 PUM 数据。两种排序方式确实存在分歧:ARROZ ESTÁNDAR 500 GRS 是总价最便宜的,售价 $1.550,每公斤 $3.100,而同一搜索结果中最具性价比的是每公斤 $2.775——来自一包 $5.550 的袋子,它根本不会出现在总价前五名中。按总价排序会给出更差的答案,尽管看起来正确。
由于 D1 的搜索无法按此排序,--sort per-unit 仅对您已获取的页面进行排序,而非整个结果集。增加 --count(最大 50)以扩大范围。输出会明确说明这一点,而非暗示无法支持的绝对最优,并指出有多少结果未发布规格而无法比较。
多包装的声明规格是整包规格,而非单件规格——不要 "更正" 它。 对所有 1,600 种商品的普查发现,在名称可匹配的多包装中,D1 足以做出判断的 46 种里有 44 种声明的是整包总量。从产品名称中解析出 N UN 并乘以单件规格会损坏这 44 种以修复 2 种。名称中的数量并非 PUM 含义的证据。
因此,resolvePackSize 读取的是描述而非名称。在所有 1,548 种带有 PUM 的商品中,它对描述中同时说明了单件规格和包装数量的 10 种商品执行操作:三种被更正(SKU 718、510、1008),七种被确认为已正确声明整包规格,其余商品保持原样。
阅读警告标签。 产品带有哥伦比亚的前置包装警告(Exceso en Azúcares、Exceso en sodio、Exceso en grasas saturadas、Exceso en grasas trans、Contiene Edulcorantes),位于 --json 输出的 warnings[] 中。覆盖率不完整——按类别大约 70–90%——因此缺失意味着 "未声明",而非 "安全"。
然后在使用 d1 facets <query>(按类别、品牌、子类别统计数量)缩小范围后再进行分页:搜索最多 50 页,因此宽泛的查询最好通过分类来缩小,而非逐页浏览。
命令
| 命令 | 功能 |
|---|---|
region --lat --lng | 解析并记住配送地点 |
search <query> | 查找商品(--facets --sort --count --page --available) |
suggest <partial> · trending | 自动补全 · 哥伦比亚的热搜商品 |
categories · facets [query] | 部门树 · 查询的筛选条件 |
substitute <sku> | 推荐的替代商品及其变更(--limit --count) |
quote <sku>[:qty]... | 为购物篮定价,不修改购物车 |
cart [show|add|set|clear|deliver-to|checkout] | 构建购物篮 |
login --email / --from-cookie · whoami · logout | 账户管理 |
orders · order <id> | 订单历史(详细信息被白名单限制;--raw 选择加入) |
stores near | 附近的 D1 门店——仅限位置信息,D1 不提供自提服务 |
测试
bun test # 货币单位、分号陷阱、报价中的数量、未知SKU的处理、
# 购物车标准化、订单信息脱敏、以及限制支付范围的端点白名单
bun run lint && bun run typecheck
退出码是约定而非装饰——调用 部分下的表格是其唯一副本。
完整端点映射、配送层级行为以及公共 API 无法提供的内容,请参见 README.md。