# Fbo Orchestrator

> Ozon FBO 端到端发货编排. 当用户说"丝绸生活账号补 N 天 FBO"、"按安全天数发货"、"清空海外仓"、"补海外仓没货的 SKU"、"Q_X 发 N 箱你帮分集群"、"全自动发货"、"FBO 一键发"时触发. 自动 4 mode 决策 + 跑 create_fbo_plan + cron-style 自动 retry 直到全发完, 终态出对照表 Excel. wrapper 调用老 fbo-* 子 skill 的脚本, 不重复实现.

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

---


# FBO Orchestrator — 一键发货编排

**5 个老 skill (`fbo-fill-boxes` / `fbo-plan` / `fbo-retry` / `fbo-service-up` / `fbo-status`) 仍单独可用** — 本 skill 只是把它们组合成端到端流程, 处理决策 + 自动 retry + 终态报告.

## 4 种触发 mode

| 用户说法 | mode | 触发条件 |
|---|---|---|
| "丝绸生活补 60 天", "按安全天数发" | `safety_days` | 用户给 SKU 列表 + 想要的目标天数, 走 4180 算每集群 gap |
| "清空海外仓", "全部发 FBO" | `force_dispatch` | 走老 `/ozon-restock-force-dispatch` skill (Bitable HITL) |
| "Q_MeiGongDao 24 箱, Q_ChongQiZui 12 箱, 帮我分集群" | `manual_matrix` | 用户给 `{SKU: 总箱数}`, skill 按各集群 need_pieces 占比分摊 |
| "海外仓没货的 SKU 都补一下" | `missing_sku_top_up` | 4180 inventory==0 的 SKU 用 4181 plan.allocations 全发 |

## 入口脚本

`/Users/mac/.claude/skills/fbo-orchestrator/orchestrate.py` (~250 行) — 出 `plan.json` 喂下游.

```bash
# safety_days
python3 ~/.claude/skills/fbo-orchestrator/orchestrate.py \
  --mode safety_days --account 丝绸生活 \
  --skus Q_MeiGongDao-HeiSe-Free,Q_ChongQiZui-HuangSe-Free \
  --out /tmp/orch_plan.json

# manual_matrix (用户给总箱数, skill 分集群)
python3 ~/.claude/skills/fbo-orchestrator/orchestrate.py \
  --mode manual_matrix --account 丝绸生活 \
  --boxes 'Q_MeiGongDao-HeiSe-Free=24,Q_ChongQiZui-HuangSe-Free=12' \
  --out /tmp/orch_plan.json

# missing_sku_top_up
python3 ~/.claude/skills/fbo-orchestrator/orchestrate.py \
  --mode missing_sku_top_up --account 丝绸生活 \
  --out /tmp/orch_plan.json

# force_dispatch — 不出 plan.json, 跳转到老 skill
python3 ~/.claude/skills/fbo-orchestrator/orchestrate.py \
  --mode force_dispatch --account 丝绸生活
# 然后在 session 里调 /ozon-restock-force-dispatch
```

输出 plan.json 跟 `list_pending --emit-plan` 同结构 (3 个键: `source_warehouse_keyword` / `drop_off_keyword` / `matrix`), 直接喂 `create_fbo_plan.py`.

## SOP (Claude 在 session 里编排)

**Step 0 — 健康检查**:
```bash
curl -s http://localhost:4180/api/health 2>/dev/null || curl -s -o /dev/null -w "4180 %{http_code}\n" http://localhost:4180/api/rows?date=2026/04/26
curl -s -o /dev/null -w "4181 %{http_code}\n" http://localhost:4181/docs
rtk proxy curl -s http://localhost:4182/health
```
任何一个挂 → push 报警退. 不需要全 200, 但至少 4181/4182 要活 (4180 必备 if mode in [safety_days, manual_matrix, missing_sku_top_up]).

**Step 1 — 选 mode + 跑 orchestrate.py**:
按用户说法定 mode (上面表), 跑 orchestrate.py 出 `/tmp/orch_plan.json`.

如果 mode = `force_dispatch`, 跳到 `/ozon-restock-force-dispatch` skill, 等用户 Bitable 审批后再继续 — 此时本 skill 暂停, 等用户回到本 skill 继续 retry.

**Step 2 — dry-run 检查**:
```bash
cat /tmp/orch_plan.json | python3 -c "import sys, json; d=json.load(sys.stdin); m=d['matrix']; print(f'集群 {len(m)}, SKU {len({s for c in m.values() for s in c})}, 总 {sum(q for c in m.values() for q in c.values())} 件')"
```
让用户看一眼 — 如果数字明显不对 (如 manual_matrix 给了 24 箱但分到 0 集群), 拒跑.

**Step 3 — 跑 create_fbo_plan**:
```bash
cd /Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货
nohup python3 create_fbo_plan.py --config /tmp/orch_plan.json --box-size 300 --account <账号> > /tmp/orch_run_$(date +%H%M).log 2>&1 &
```
Monitor PID 退出, grep `alloc|fallback:|state=|reason=|order_id|✓|✗|!|shipped=|skipped=|完成|done|scoring|cancel|实接|shift|Traceback`.

**Step 4 — 解析 ledger**:
```bash
LEDGER=$(ls -t ledger/shipment_ledger_*.json | head -1)
python3 -c "import json; d=json.load(open('$LEDGER')); print(f'shipped: {len(d[\"shipped\"])} orders / {sum(sum(it[\"quantity\"] for it in s[\"items\"]) for s in d[\"shipped\"])} 件; skipped: {len(d[\"skipped\"])} 条 / {sum(s[\"quantity\"] for s in d[\"skipped\"])} 件')"
```

**Step 5 — 自动 retry loop**:
- 用 `/loop 1h <retry-prompt>` 启动每小时 cron (复用 fbo-retry skill 的模板)
- 每轮: list_pending --emit-plan → create_fbo_plan → 解析 ledger
- 失败原因分类:
  - `NOT_AVAILABLE_MATRIX` / `NOT_AVAILABLE_ROUTE`: 静默 retry (24h+ 才有意义松开)
  - 限流踩 > 半数: push "限流踩 N/8, 考虑加严限流"
  - 实接 < box/2 的 supply 自动 cancel (`MIN_BOX_FILL_RATE`, 80h shift 规则)
  - 其他错误 (网络/4182 503): 静默
- 直到 `python3 list_pending.py` 输出 0 条 → push "✅ <账号> 全发完, 共 X 件 / Y 集群"

**Step 6 — 终态报告 (双视角)**:

(a) **按集群×SKU 决策视角** — 计划/已发/差额/状态 (老板/决策视图):
```bash
python3 build_shipment_overview.py --exclude-before <run_start_iso>
```
"改派抵扣已完成" 状态自动展示.

(b) **按发运日期实操视角** — 司机交货前打印用 (调度/实操视图):
```bash
python3 build_by_date.py
# 或显式状态: --states "READY_TO_SUPPLY,SHIPPING_PREPARED"
```
输出 `by_date/YYYY-MM-DD/{对照表.xlsx, 箱唛合并.pdf}`. 默认只取 Ozon 后台 "准备交货" tab (READY_TO_SUPPLY), 同日所有 supply 的箱唛 PDF 合并为 1 个文件方便打印.

## 决策算法 (orchestrate.py 内)

### safety_days (per-cluster gap)
```
对每 (sku, cluster):
  target  = safety_days * daily_sales       (4180 /api/rows[].FBO安全天数 × 每日销量)
  on_hand = FBO上架数量 + FBO越库在途数量
  gap     = max(0, target - on_hand)
  effective = floor_box(gap, box_size, ≥ box/2 残箱保留, 否则砍)
matrix[cluster][sku] = effective
```

### manual_matrix (用户给总箱数, skill 分配)
```
for sku, total_boxes in user_input:
  total_pieces = total_boxes * 300
  rows = 4180 /api/rows?sku=X
  weights = need_pieces / sum(need_pieces)        # 优先按集群缺口比例
                                                   # 兜底 daily_sales 比例 (无缺口时)
  按 weights 大→小遍历, 每集群 alloc = total_pieces * weight
                                              + floor_box (≥ box/2 残保留)
  remaining 在最后集群兜底
```

### missing_sku_top_up
```
plans = 4181 /plans?account=X (status in approved/dispatched, 取最新 created_at)
for sku, plan in plans:
  if 4180 /api/overseas_inventory?sku=X .value > 0:
    skip
  else:
    detail = 4181 /plans/<plan_id>
    for alloc in detail.allocations:
      matrix[alloc.cluster][sku] = floor_box(alloc.pieces)
```

### force_dispatch (跳老 skill)
不出 plan.json, stderr 提示用户在 session 里走 `/ozon-restock-force-dispatch`.

## 常见问题

### Q1: safety_days 跑出来 matrix 空
- 可能所有集群都已超 safety 上限 (一般是节后) → 不需补
- 4180 服务挂 (`/api/rows` 拿不到) — 检查 4180 health
- SKU 名错 (4180 找不到) — 看 stderr

### Q2: manual_matrix 分配不均匀
- 算法按 `集群需补货数量` 占比 — 4180 算法已偏好缺口大的集群
- 整箱 floor + 残箱 ≥ box/2 规则会损失一些零头, 这是用户硬规则
- 如果用户想精确分到某集群, 直接手写 plan.json 不走本 skill

### Q3: force_dispatch 之后回到本 skill 怎么继续 retry
跑完 ozon-restock-force-dispatch 后, 4181 plans 已 approved/dispatched. 直接进 Step 5 retry loop (list_pending 会捡起未发的).

### Q4: 多账号怎么搞
一次只跑一个账号 (跟老 skill 一致). 跨账号: 用户连续起两个 cron, 各自带 `--account`.

### Q5: 不同 box_size 的 SKU
本 skill 默认全部 300 (--box-size 覆盖). 若混 SKU 不同 box_size, 用 4181 plan-id 模式: orchestrate.py 出 plan.json 后, 跑 `create_fbo_plan.py --plan-id <pid1>,<pid2>` (绕开 cfg 模式), box_size 自动按 plan.

## 同步 .py 副本

orchestrate.py 是 skill 自带的, 不会被 sync 脚本动. 改 orchestrate.py 直接编辑 `~/.claude/skills/fbo-orchestrator/orchestrate.py`.

工作目录的 list_pending.py / create_fbo_plan.py 改了用 `bash ~/.claude/skills/sync_fbo_scripts.sh` 同步到老 5 个 skill (本 skill 不依赖副本).

## 相关 skill / memory

- `/fbo-service-up` — 4182 docker 运维
- `/fbo-plan` — 直接跑 plan.json (本 skill Step 3 调它的脚本)
- `/fbo-retry` — list_pending 算法 (本 skill Step 5 复用)
- `/fbo-fill-boxes` — 单 order fill (--skip-fill 后补流程)
- `/fbo-status` — build_shipment_overview (本 skill Step 6 调)
- `/restock-to-fbo` — 单 SKU safety_days (本 skill 多 SKU 版的灵感来源)
- `/ozon-restock-force-dispatch` — force_dispatch 走老 skill 链 (Bitable HITL)
- memory `reference_restock_api.md` — 4180 端点契约
- memory `reference_fbo_rs_integration.md` — 4181 plan 模型
- memory `reference_create_fbo_plan_sop.md` — create_fbo_plan SOP 6 步
- memory `feedback_partial_box_residue_rule.md` — 整箱 floor + 残箱 ≥ box/2
- memory `feedback_safe_cancel_80h_rule.md` — 取消 supply 必先 shift ≥80h
- memory `feedback_reallocate_to_moscow_capacity.md` — per-SKU 总量算法 + 改派抵扣

