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 會從全國目錄中回答,針對虛擬賣家 1 回報舒適的 10,000 件庫存。將同一個 SKU 放入針對真實地址的結帳模擬中,它會回傳 withoutStock,因為實際負責運送的那家門市沒有庫存。請務必先使用 d1 region;當 CLI 顯示的是全國價格時,它會以純文字的形式發出警告。
解析區域也是 API 中最難猜測的部分:座標必須以 geoCoordinates={lon};{lat} 的格式傳入——以分號分隔,經度在前。VTEX 自己的 JSON 主體中使用的是逗號,但此處使用逗號會被以 CHK0119(「地址必須包含郵遞區號或地理座標」)拒絕——這則訊息看起來像是參數缺失,而非格式錯誤。test/region.test.ts 固定了此行為,以防止後續清理時意外更改。
價格以兩種單位呈現。 對於 SKU 262(Latti 全脂牛奶 900 毫升,貨架價格為 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 '../../../..' 可以從搜尋端點解析到訂單結算,而所有原始碼的關鍵字仍然是核准的。
準確地說,因為這個檔案的早期版本過於自信:沒有什麼可以阻止具有寫入權限的提交者編輯白名單。儲存庫內的測試無法做到這一點。
它儲存的唯一憑證是商店前端的會話 Token——與已登入瀏覽器持有的相同,範圍限定於其擁有者的訂單和購物車——以模式 0600 寫入 ~/.config/d1-cli/session.json。任何地方都不會讀取 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 表示沒有——這是與 d1 basket 並列,唯二使用退出碼 3 的指令。
使用 3 而不是 1,其區別是關鍵所在:在 CLI 的其他地方,1 表示 「D1 拒絕連線,或無法連線」,並允許重試。一個空的分類永遠不會因為重試而變得非空,因此使用重試-1 策略的代理人會永遠循環。退出碼 0 伴隨著空列表是另一個錯誤答案——它會被視為成功並被執行。
每個候選商品都會說明改變了什麼:品牌、包裝尺寸、每公斤或每公升價格、包裝價格,以及任何新增或移除的 Ley 2120 警告。這是輸出內容,不是分數。相同的 900 毫升和相同標籤下,每公升便宜 -12% 是一個不同的方案,而附帶 Exceso en Azúcares 警告的又是另一個方案,只有差異才能區分它們。
僅供推薦,請勿直接採用。 substitute 永遠不會寫入——它會輸出 d1 cart add 命令供人類核准。這並非為了謹慎而謹慎:顧客兩次都明確表示偏好移除某項商品,而不是接受錯誤的替代品,而且兩次都是對的。請呈現候選商品及其利弊;讓使用者決定,並願意取消該商品。
有三件事值得關注:
- 「此商品在您的門市仍有庫存。」 庫存是依門市而異的,因此其他門市缺貨的商品,此門市可能還有。在替換任何商品之前請先檢查。如果沒有提供
--lat/--lng,它會這樣說,而不是猜測——因為沒有門市可以做出基於門市的庫存聲明。 - 「擴展到 3 個層級中的第 2 級」——葉子分類中沒有庫存,因此建議來自更廣泛的通道,相應地也比較寬鬆。
- 「僅比較了 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
退出碼是一種合約,而非裝飾——上方 執行 下的表格是其唯一的副本。
請參閱 README.md 了解完整的端點對應表、運送層級行為以及公開 API 不會提供的內容。