# Fbo Retry

> Ozon FBO 未发清单查询 + 生成重试 plan (list_pending.py). 当用户说"查未发"、"看 pending"、"哪些集群还没发"、"把未发的改派到其他集群"、"重试失败的"、"生成 retry plan"、"未发到哪些集群了"、"矩阵放开了重试"时触发。支持 by-cluster/by-sku/by-reason 分组, 过滤, 改派, 一键生成新 plan.json 灌回 `/fbo-plan`.

- Skill: `yulianggan/fbo-retry` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add yulianggan/fbo-retry`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yulianggan/fbo-retry/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: yulianggan (https://skillmd.com/u/yulianggan)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yulianggan/fbo-retry

---


# FBO 未发清单查询 + 重试

**从 ledger 台账提炼当前未发清单**, 支持按集群/SKU/原因分组查看, 可直接改派或生成新 plan.json 让 `/fbo-plan` 重试.

脚本: `/Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货/list_pending.py` (~180 行)

## 触发场景

- "看一下还有哪些未发"
- "哪些 SKU 还没发 / 哪些集群还没发完"
- "把远东的未发改派到莫斯科"
- "把新西伯利亚的 Q_MeiGongDao 1800 件生成个 retry plan"
- "按原因看 — 矩阵过滤的 vs 业务拒绝的"
- "矩阵应该松了, 重试那批"

## 工作原理

**两个数据源 (2026-04-25 起默认从 4181 plans 算)**:
1. **`--from-approved`** (默认 ON): 4181 service `/plans?status=approved/dispatched` 的 cluster→SKU→件数 cap_total, 减去 Ozon `/v1/supply-order/list`+`bundle` 的 active_total (该 SKU 全集群已发件数)
2. `--from-ledger`: 老逻辑 — 扫 `ledger/shipment_ledger_*.json` skipped 记录

**per-SKU 总量算法 (2026-04-25 fix, 处理改派抵扣)**:
```
对每个 offer_id:
  cap_total    = sum(4181 plans 该 SKU 各 cluster 件数)
  active_total = sum(Ozon 实发该 SKU 各 cluster 件数, 含 4181 计划外的 "改派目标" cluster)
  if active_total >= cap_total: 整 SKU 跳过 (含改派抵扣)
  else: per-cluster gap, 上限 remaining=cap_total-active_total
```
旧 per-cluster 算法不识别改派 (远东 600 → Москва 后, 远东 cap 600 - active 0 = 误报 600 仍 pending). 算法落地后 7 集群 6300 件 → 6 集群 5700 件 (远东 Q_ChongQiZui 自动消解).

**bundle retry-with-backoff** (2026-04-25): `_post()` 包 3 次 backoff (2/4/6s) 防止 cron 偶发 `/v1/supply-order/bundle` timeout 漏算 active.

输出:
- 平铺列表 / 按集群 / 按 SKU / 按原因分组
- 可选: 改派 (源集群→新集群) + 生成新 plan.json 喂回 `create_fbo_plan.py`

## 常用用法

工作目录统一:
```bash
cd /Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货
```

### 1. 看所有未发 (平铺)
```bash
python3 list_pending.py
```

### 2. 按集群分组 (最常用)
```bash
python3 list_pending.py --by-cluster
```

### 3. 按 SKU 分组 (看哪个 SKU 发不动)
```bash
python3 list_pending.py --by-sku
```

### 4. 按原因分组 (MATRIX_DROPPED_FROM_SUPPLY / NO_AVAILABLE_WAREHOUSE / DRAFT_OR_SUPPLY_FAILED)
```bash
python3 list_pending.py --by-reason
```

### 5. 过滤: 只看某原因
```bash
python3 list_pending.py --filter-reason MATRIX_DROPPED_FROM_SUPPLY
python3 list_pending.py --filter-reason NO_AVAILABLE_WAREHOUSE
```

### 6. 过滤: 只看某集群 (substring 匹配)
```bash
python3 list_pending.py --filter-cluster Новосибирск
python3 list_pending.py --filter-cluster "Дальний"
```

### 7. 改派: 把某集群的未发改发到其他集群
```bash
python3 list_pending.py --reallocate "Дальний Восток=Москва,Новосибирск=Казань"
```
语法: `源集群=新集群,源2=新2`. 会改 cluster 字段但不影响 ledger.

⚠️ **`--reallocate` 参数 bug**: 集群名含 `,` (例如 `"Москва, МО и Дальние регионы"`) 会被 split 截断成 `"Москва"`. 解决: 直接手工写 plan.json 不走 `--reallocate`, 或换其他不含逗号的集群名 (如 `Москва`).

**改派 Москва 是矩阵拒/限流挡集群件数的安全出口** (2026-04-25 实测):
- 远东 Q_ChongQiZui 600 → `Москва, МО и Дальние регионы` 全 600 件实接 (无 trim)
- 跟 Саратов/Казань `Q_MeiGongDao` 跨多轮 hard cap ~52-58 件强烈对比 — Москва 头部集群 reserved 多
- 改派后跑算法: per-SKU 总量自动抵扣, 远东 cap 600 vs active 0 但 Москва active 600 → 整 SKU `cap_total == active_total` 跳过

### 8. 生成重试 plan.json (核心场景)
```bash
python3 list_pending.py --emit-plan retry.json
# 之后:
python3 create_fbo_plan.py --config retry.json --box-size 300 --account 丝绸生活
```

### 9. 改派 + 生成 plan (一气呵成)
```bash
python3 list_pending.py \
  --reallocate "Дальний Восток=Москва" \
  --emit-plan retry_moscow.json
```

### 10. 只重试矩阵过滤的那一批
```bash
python3 list_pending.py \
  --filter-reason MATRIX_DROPPED_FROM_SUPPLY \
  --emit-plan retry_matrix.json
```

### 11. 自定义 drop-off / source (默认 ЩЕРБИНКА / ЖУКОВСКИЙ_РФЦ)
```bash
python3 list_pending.py \
  --emit-plan retry.json \
  --drop-off-keyword "КАЗАНЬ_ХАБ" \
  --source-warehouse-keyword "ЖУКОВСКИЙ_РФЦ"
```

## 典型工作流

### A. 本次 run 完, 看剩多少没发 → 决定等等还是改派
```bash
python3 list_pending.py --by-reason
# 如果都是 NO_AVAILABLE_WAREHOUSE (业务矩阵拒), 等几天
# 如果是 MATRIX_DROPPED_FROM_SUPPLY, supply 建了但过滤了, 重试可能还是过滤
# 如果是 DRAFT_OR_SUPPLY_FAILED, 看具体 reason, 可能是 timeslot 问题, 重试
```

### B. 某集群几天后解锁, 重发那批
```bash
python3 list_pending.py --filter-cluster Новосибирск --emit-plan retry_nsk.json
python3 create_fbo_plan.py --config retry_nsk.json --box-size 300 --account 丝绸生活
# 成功发的自动从 pending 消失 (下次 list_pending 看不到)
```

### C. 业务决定某集群死活发不动 → 改到其他集群
```bash
python3 list_pending.py --reallocate "Дальний Восток=Москва" --emit-plan retry_moscow.json
python3 create_fbo_plan.py --config retry_moscow.json --box-size 300 --account 丝绸生活
```

## 输出示例 (by-cluster)

```
=== pending 3 条 (过滤后) ===

[Дальний Восток] (2 条)
  Q_ChongQiZui-HuangSe-Free (3214934161) × 300  — NO_AVAILABLE_WAREHOUSE
  Q_MeiGongDao-HeiSe-Free (3214793665) × 900  — NO_AVAILABLE_WAREHOUSE
[Новосибирск] (1 条)
  Q_MeiGongDao-HeiSe-Free (3214793665) × 1800  — NO_AVAILABLE_WAREHOUSE
```

## reason_code 枚举 (ledger)

- `MATRIX_DROPPED_FROM_SUPPLY` — Ozon 建 supply 时静默过滤 SKU, supply 实际不含
- `NO_AVAILABLE_WAREHOUSE` — timeslot/info `NotFound "can't find available warehouse scoring result"` (业务矩阵全拒)
- `DRAFT_OR_SUPPLY_FAILED` — 其他 draft/supply 建单失败 (伴原始错误)

## 注意

- `--emit-plan` 产物可直接喂 `create_fbo_plan.py`, 无需手改.
- 重试前可以先 dry-run 看可发性: `create_fbo_plan.py --config retry.json ... --dry-run`.
- pending 不是"永远发不掉", 只是**该 run 当时发不掉**. 业务矩阵松开可重发, 海外仓清了也可重发.
- 改派后原 ledger.skipped 记录不动, 仅当次 emit 的 plan.json 里集群被替换.

## 每小时 cron retry 工作流 (本 session 验证)

矩阵拒/route 拒集群没法手工干预, 但 Ozon 矩阵动态调整, 24h+ 后可能放开. 配 cron 每小时 retry, 新放开的集群自动建单 + 填箱 + 拉 PDF.

```bash
# 在 Claude Code session 里 (不是系统 cron):
/loop 1h "每小时 retry FBO 失败集群. 步骤: 1) 健康检查 4182+4181; 2) python3 list_pending.py --emit-plan /tmp/retry_hourly.json; 3) 空就 push '清空' 退出; 4) 后台跑 create_fbo_plan; 5) Monitor 直到 PID 退出; 6) 解析 ledger, shipped>0 push, 全 skipped 矩阵拒不 push (避免半夜骚扰); 7) 不要问用户, 直接执行."
```

**实测节奏** (本 session 跨 16+ 轮):
- 第 6 轮 Саратов 突破 (52 件 → 600 件全接) — capacity 是动态的
- 第 N 轮 Казань 矩阵开 → 600 件实接 (FULL_AVAILABLE)
- Махачкала/Ярославль/Воронеж 持续 NOT_AVAILABLE_MATRIX (24+ 小时仍拒, 等更久)
- Новосибирск NOT_AVAILABLE_ROUTE (路由禁, 比矩阵更顽固)

**12h 短间隔实测无效**, 至少 24h+ 才有意义 (`feedback_multicluster_matrix_short_circuit.md`).

## 同步本 skill 的 .py 文件

工作目录是真理之源. 修改 `list_pending.py` / `cancel_supply.py` 后:
```bash
bash /Users/mac/.claude/skills/sync_fbo_scripts.sh           # 推全部
bash /Users/mac/.claude/skills/sync_fbo_scripts.sh --check   # 只看哪个 diff
```

## 相关 skill / memory

- `/fbo-plan` — 跑 plan.json (emit 完直接喂它)
- `/fbo-status` — 看已发/未发全景, 不只是 pending
- memory `reference_shipment_ledger.md` — ledger + pending 去重规则详解
- memory `reference_create_fbo_plan_sop.md` — SOP 6 步, 理解为什么会产生哪种 reason_code
- memory `feedback_reallocate_to_moscow_capacity.md` — 改派 Москва 安全出口 + per-SKU 总量算法
- memory `feedback_list_pending_bundle_retry.md` — bundle 必须 retry-with-backoff
- memory `feedback_multicluster_matrix_short_circuit.md` — 矩阵拒不 fallback + 24h+ retry 才有效

