# KB-AMAZON-002：Amazon US 库存与卖家监控技术实录

| 项目 | 内容 |
|---|---|
| 平台/站点 | Amazon 美国站 `www.amazon.com` |
| 对应 Tool | `amazon_monitor_inventory_seller` |
| 对应脚本 | `amazon-inventory-monitor.user.js` 1.3.0 |
| 整理日期 | 2026-09-14 |
| 验证方式 | 真实 Amazon 浏览器会话、脱敏/合成 DOM、模拟购物车与飞书 API、完整仓库自动化测试 |
| 当前证据等级 | 已验证部分真实商品与当前页面结构；不是 Amazon 官方库存 API，不保证所有账号、地区、商品和 A/B 页面永久适用 |

本文记录库存与卖家监控脚本从需求分析到实测发布遇到的关键问题。重点不是某个固定 ASIN 的结果，而是怎样区分页面展示、购物车购买上限、卖家可售数量和证据不足，避免把一个看起来合理的数字误报为精确库存。

## 1. 数据边界：监控到的是什么

脚本得到的是当前 Amazon 会话、配送地址、时间和目标 offer 下的卖家可售证据，不是仓库物理库存，也不是 Amazon Seller Central 的官方库存字段。价格、Buy Box、配送方式、限购、地区限制和登录状态都会影响结果。

库存结果必须带证据等级：

| certainty | 含义 | quantity | lower_bound |
|---|---|---|---|
| `exact` | Amazon 响应明确给出目标卖家的可售数量 | 精确值 | 同一值 |
| `at_least` | 只证明库存不少于某个数量，常见于限购或探测上限 | `null` | 已证明的下限 |
| `out_of_stock` | 页面或购物车响应明确无货 | `0` | `0` |
| `unknown` | 页面、卖家或响应证据不足 | `null` | 可能为 `null` 或已观察到的最小值 |

不能把 `unknown` 写成 0，也不能把购买限制当库存。脚本没有销量字段，也不通过库存差倒推销量。

## 2. 输入与目标卖家

输入接受 10 位 ASIN，或包含 `/dp/{ASIN}`、`/gp/product/{ASIN}` 的 Amazon US 链接。链接里的 `smid` 可以指定卖家，显式 `seller_id` 优先；Seller ID 留空时监控当前 Buy Box 卖家。

商品页先读取当前 Buy Box offer。指定卖家与 Buy Box 不同时，再请求 `?smid={SELLER_ID}&psc=1` 的卖家页面。加入购物车所需的 ASIN、`merchantID`、`offerListingID` 和其他成功表单控件来自当前 offer 的购买表单，不能只拼 ASIN，否则可能探测到另一个卖家。

## 3. 为什么“加购 999”不够

常见做法是把购物车数量改成 999，再读取“Only N available”。它只能覆盖较小库存，无法解释库存大于 999、购物车行上限、卖家限购和站点防滥用响应。

当前实现的探测顺序是：

1. 读取探测前购物车，记录目标 ASIN、Seller ID、item ID 与原数量。
2. 购物车没有目标 offer 时，按商品页真实表单临时加入 1 件。
3. 从购物车页面读取 `anti-csrftoken-a2z` 和活动商品数据。
4. 调用购物车数量更新 XHR，第一档提交 `1,000,000`。
5. 若只命中购物车行的 1000 上限，刷新购物车并用 `2,147,483,647` 再探测一次。
6. 无论探测成功或失败，都进入清理：本次新增的商品删除；原本已有的商品恢复原数量。

这里的“协议调用”是复用浏览器当前会话，按 Amazon 页面自己使用的表单/XHR 结构发请求；它不是 Amazon 官方对外库存 API。接口和 DOM 都可能变化，必须保留超时、HTTP 状态、Robot Check 和结构异常处理。

## 4. 大于 999 的核心坑：商品行 1000 不等于库存 1000

真实测试发现，Amazon 对超大数量请求可能在同一个 JSON 响应中给出两类互相看似矛盾的信息：

- `features.active-cart`：购物车商品行被硬限制为 1000，并可能显示类似 “only 1000 available”。
- `features.imb`：重要提示中出现 “than the N available from the seller”，其中 N 才是本次响应明确披露的卖家可售数量。

因此解析优先级必须是：

1. 先解析 `features.imb` 中明确的 seller available quantity。
2. 再识别购买限制，返回 `at_least`，不能冒充精确库存。
3. 再识别明确无货。
4. 最后才看 `features.active-cart` 和响应后的购物车数量。

当探测值大于 1000、商品行恰好返回 1000 时，只能得出“至少 1000”或继续升级探测，不能返回 `exact=1000`。这是本次开发最关键的正确性修复。

2026-09-12 在配送邮编 91789 的真实会话中，脚本曾从 `features.imb` 得到以下大库存精确证据：`B082314NFL = 40076`、`B0GWFN7Y8T = 2208`、`B07T869RNY = 200079`。同一商品稍早/稍后的读数可能变化，这符合当前卖家可售量而非固定常量的语义。

## 5. 购物车安全与清理

库存探测会临时改变购物车，所以清理不是附加功能，而是事务的一部分：

- 探测前已存在同一 ASIN 和 Seller ID：记录原数量，结束时恢复。
- 探测任务新加入：结束时删除该 item ID。
- 加购后没找到准确卖家：停止探测，仍尝试删除本次新增候选商品。
- 主流程抛错：`finally` 仍执行清理。
- 清理响应缺少确认：返回警告，要求人工检查，不宣称清理成功。

匹配时必须同时考虑 ASIN、Seller ID 和购物车 item ID。只按 ASIN 删除，可能误删用户原有的另一个卖家 offer。

## 6. 跟卖：不能直接使用 “New (N)”

商品页的 “Other sellers on Amazon / New (N)” 更接近报价入口数量，并不保证等于唯一卖家数。真实 `B07T869RNY` 页面入口显示 `New (4)`，但 All Offers HTML 只解析到 3 个唯一 Seller ID。

脚本请求页面实际使用的 All Offers 地址：

```text
/gp/product/ajax/aodAjaxMain/ref=dp_aod_NEW_mbc?asin={ASIN}&...&pc=dp
```

请求带 `X-Requested-With: XMLHttpRequest`，从每个 `#aod-offer` 的卖家链接或 `merchantID` 读取 Seller ID，再按 Seller ID 去重。跟卖基准为显式 Seller ID；未指定时为当前 Buy Box Seller ID。

返回字段包括当前唯一在售卖家总数、去除基准卖家后的跟卖数、是否有跟卖，以及跟卖名称和 Seller ID。All Offers 被限流、挑战页或结构改版时，跟卖状态返回 `unknown`，不能把“没解析到”写成“无跟卖”；库存探测本身仍可继续。

## 7. 商品页跟进信号

Review 读取商品页当前显示的 Review/Rating 总数，只返回 `review_count` 当前值。脚本不计算 `review_change`，也不维护独立 Review 历史。

类目排名读取 Best Seller badge 和商品详情中的 Best Sellers Rank。一个商品可能同时有大类和小类排名，因此返回 `category_ranks` 数组；写入飞书时格式化为 `#名次 类目`，多个排名用中文分号分隔。没读到时返回空数组，不编造排名。

1.3.0 起还保留 `parent_asin`、`variation_attributes`、`rating`、`coupon_text`、`delivery_text` 和 `return_warning_text`。这些都是当次商品页可见快照：没有页面证据时返回 `null`、空对象或空数组，不猜测父体、促销、配送或退货风险。这些字段用于定时跟进快照，脚本本身仍不保存历史、不计算变化值。

## 8. 历史飞书直写经验与 TextFieldConvFail

以下记录旧版脚本的直写实现。当前官网库存脚本 1.3.5 已删除相关参数和代码，按 v2 返回业务数据，回传统一由数据中心执行；旧缓存、全文本列、读后重试规则不适用于现行脚本。

SellerFeeds 的批量任务会把多个 ASIN 展开后逐项串行执行；库存脚本每个 ASIN 向飞书追加一行。批量失败不一定是并发问题。

一次真实反馈是单条正常，批量到某些 ASIN 时出现：

```text
HTTP 200; code=1254060, msg=TextFieldConvFail
```

原因是某些条件字段只在部分商品有值。用户复制表头后，飞书默认创建文本列；当后续 ASIN 首次出现价格、精确库存等数字值时，向文本列发送 JSON number 才触发转换失败，所以看起来像“批量才失败”。反方向也可能出现 `1254061 NumberFieldConvFail`。

从 1.3.0 起，库存脚本的 32 个飞书字段全部声明为文本，写入层也默认把数字转成字符串。新用户选择飞书表格后，脚本在权限允许时自动补齐缺失文本列。Tool 的 JSON 输出仍保留 number/boolean 语义，只有飞书展示层文本化。

用户输入只显示 `baseUrl` 和 `baseToken`：两项留空则仅返回监控结果，不请求飞书；只填一项则在采集前拒绝；完整填写则自动追加。飞书机器人发送任务摘要和报告文件；成功写表且返回有效地址时，摘要可附多维表格链接。写表失败时保留采集结果和报告文件。

为兼容按旧版说明建成数字列的表格，运行时在收到 `1254060/1254061` 后会读取真实字段列表：文本列转字符串，数字列转有限数字，然后重试；字段类型缓存 5 分钟供同批后续 ASIN 复用。缺字段或遇到不支持的字段类型时返回具体列名。

## 9. 限流、挑战页与失败语义

- HTTP 429：返回明确限流错误，建议降低批量/定时频率。
- HTTP 401/403：提示在浏览器完成 Amazon 登录或验证。
- Robot Check/CAPTCHA：返回 `AMAZON_CHALLENGE`，不把挑战页当商品页。
- 请求超时：每个 Amazon 请求有超时保护。
- All Offers 单独失败：跟卖为 unknown，库存流程继续。
- 库存主流程失败：仍执行购物车清理，然后抛出原始错误并附清理警告。

定时任务不宜为了“更实时”而高频运行。多个 ASIN 的批量规模、间隔和账号会话应以不触发 Amazon 限流或验证为前提。

## 10. 代码、测试与复现入口

| 位置 | 作用 |
|---|---|
| `examples/shared/amazon-inventory-monitor.mjs` | 输入、商品页、All Offers、购物车请求、库存证据分级和清理 |
| `scripts/generate-amazon-userscripts.mjs` | Tool Schema、飞书 32 列声明、写入适配和正式脚本生成 |
| `examples/amazon-inventory-monitor.user.js` | 生成后的可安装官方脚本 |
| `tests/amazon-inventory-monitor.test.mjs` | DOM、All Offers、大小库存、清理、Seller ID、飞书单条/批量兼容测试 |

推荐验证顺序：

1. `pnpm run generate:amazon`
2. `node --test tests/amazon-inventory-monitor.test.mjs`
3. `pnpm run verify:userscript -- examples/amazon-inventory-monitor.user.js`
4. `pnpm test`
5. `pnpm run build:userscripts`
6. 用真实浏览器确认商品页、AOD 响应和购物车最终恢复状态

真实浏览器证据只能说明当时的页面和会话。选择器或请求结构变化时，应先保存脱敏证据、补测试，再调整解析器；不能为了“总能返回数字”而放宽到无证据猜值。
