# Jdy Shipping Track

> Monitors KOL shipment tracking numbers from Jiandaoyun 发货管理 and syncs them to Feishu Base. Maintains a watchlist of active POs, checks JDY daily until all tracking is filled, escalates stuck orders via 催发货 work orders, and drafts/sends tracking emails to KOLs once shipped. Since v4.7.0, Mode B also pulls tracking numbers straight from 执行工单 (new_track / 跟踪号) into Base when 发货管理's trackingno is blank (JDY doesn't auto-refresh it), so Base doesn't wait on manual refreshes. Since v4.8.0, Mode 0 (incremental sync) does the same for every shipment, not just watchlist POs. Since v5.0.0, the skill is department-shared: the Base is a single department-wide table keyed by Owner (提出人), Mode 0 runs centrally for all roster members, and each member's own agent instance runs Mode B for their own POs only (identity auto-resolved from the 部门成员 roster table). Trigger when the user says "add [PO] to watchlist", "加入watchlist", "check tracking", "check open POs", "check watchlist", "查询追踪号", "查物流", "update tracking from JDY", "初始

- Skill: `nana7536/jdy-shipping-track` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add nana7536/jdy-shipping-track`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nana7536/jdy-shipping-track/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: nana7536 (https://skillmd.com/u/nana7536)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/nana7536/jdy-shipping-track

---


# RITFIT Tracking — Jiandaoyun ↔ Base Sync + Watchlist Monitor (部门共用版)

> 🌐 **多语言（v5.0.1，2026-08-12）**：部门含中外籍成员。本技能的运行产出（daily-log、B5/B8 通知、对成员的提示）**语言跟随使用者**——中文使用者用中文，英文使用者用英文（Angeline Prasetio / Diego Isaac Rojo Molina 等）。对外内容（KOL 追踪邮件、SKU 英文名）一律英文；技术标识（SKU、RMA、record_id、命令）保持原文不翻译。KOL 追踪邮件模板 `references/tracking.html` 内已是英文，组装时按 B2 的 SKU 英文翻译规则 + Carrier 归一化规则填英文内容即可，勿因内部语言偏好改变邮件语言。

> **v5.0.0 (2026-08-11) — department-shared rebuild.** The skill no longer belongs to one person.
> One department-wide Base table holds everyone's shipments (each row carries `Owner` = the JDY
> 提出人's Chinese name + `负责人` = their Feishu user). **Mode 0 runs centrally** (one cron on the
> department admin's machine, currently 吴双's) and syncs shipments for **all enabled roster
> members**. **Mode B runs per-person**: each brand-dept member uses the same published agent in
> their own Cherry Studio; the agent auto-resolves "who am I" from the 部门成员 roster table (see
> the next section) and only ever touches POs whose Owner = themselves. KOL tracking emails are
> always sent from the member's own mailbox. Base write permission is enforced server-side via
> advanced permissions (see Base Config) — members can only edit `Tracking Info` / `Carrier` /
> `Email Sent?` on their own rows, so skill rules and Feishu permissions say the same thing.

Two things happen daily, on two different schedules:

1. **Mode 0 — Incremental sync** (cron `30 1,14 * * *` = 2x/day at 01:30/14:30 BJT, job
   "JDY-Base 每日自动同步", runs ONLY on the central admin machine): mirrors shipment items submitted/
   updated in JDY 发货管理 by **any enabled roster member** (提出人 ∈ roster) into the department
   Base "Shipment Tracking" table since the last run. This is the system of record — every SKU line
   becomes one Base row, tracking or not.
2. **Mode B — Watchlist check** (each member's own environment, suggested cron weekdays 3x/day
   09:00/14:00/17:00 BJT): walks that member's own watchlist, cross-references escalation work
   orders, flags stuck POs, moves finished POs to the completed log, drafts/sends tracking emails
   to KOLs **from the member's own mailbox**, and live-syncs any newly-appeared tracking numbers
   straight into Base on every check.

Mode A is manual — a human (or Mode-A-variant sync from Base) adds a PO to the watchlist.

---

## ⚠️ Identity resolution — read this FIRST on every run (v5.0)

The skill must never hardcode a person. Resolve the current user at the start of every run:

1. `lark-cli contact +search-user --user-ids me --as user --format json` → current user's
   `open_id` (and name).
2. Read the roster: `lark-cli base +record-list --base-token UGuHbNi6GapDwTs9kdUcUGZPnzS
   --table-id tblfDTCs6XeMPefA --as user --format json` (table 部门成员, fields:
   姓名 / JDY Username / Feishu Open ID / 企业邮箱 / 启用).
3. Match the current open_id against `Feishu Open ID` → get **my 姓名** (= `Owner` value on my
   rows) and **my JDY Username** (= 提出人 filter value for JDY queries).
4. If no roster row matches, or the row's `启用` is false → stop and tell the user their account
   isn't enrolled in the department shipping tracker yet (ask the admin, currently 吴双, to add/
   enable them in the 部门成员 table).
5. **Mode 0 special case**: Mode 0 only runs on the central admin machine. Its JDY filter is
   `提出人 ∈ {JDY Username of every roster row where 启用=true and JDY Username is not empty}`
   (an `in` condition), and every row it writes must carry `Owner` = 提出人姓名 AND `负责人`
   (user field) = that member's open_id (format `[{"id":"ou_xxx"}]`). Mode 0 never filters to a
   single person.

Local per-member state lives under the **current agent's own workspace** (resolve at runtime —
never hardcode another machine's absolute path): `<workspace>\_outputs\ritfit-tracking-JDY-outputs\`
holding `watchlist.md`, `completed.md`, `daily-log-DD-MM-YYYY.md`. Mode 0's cursor
`mode0-state.md` exists only on the central machine. The KOL email template ships inside the skill
at `references/tracking.html` (next to this SKILL.md) — always read it from there.

---

## Jiandaoyun Config (verified 2026-07-17)

| Field | Value |
|-------|-------|
| app_id (网红管理) | `685a468345ade02b47318ca9` |
| entry_id (发货管理) | `685bb270318253d5402ecd23` |
| entry_id (合同管理) | `685a4cab6303c86dd2eed46d` |
| entry_id (网红信息) | `685a4688ff01bd47de8c32a7` |
| app_id (产品生命周期) | `66d696158e78f315b2476b1b` |
| entry_id (单个工单) | `69ca3e985befcf33adf37ae6` |
| entry_id (批量工单) | `6a06d7fa9960ed9db0d74ace` |
| entry_id (执行工单) | `69ca3653e80a04d0ebe03a38` |

**Per-person identity (提出人 username / Feishu open_id) is NOT stored here anymore** — resolve it
at runtime from the 部门成员 roster table (see "Identity resolution" above). The roster is the
single source of truth for who is enrolled; add/remove members by editing that table, never by
editing this file.

**⚠️ The Jiandaoyun MCP server prefix is NOT stable — it changes every time the MCP server reconnects**
(observed values so far: `mcp__2k9L5gEyUsJcysSqDg6kd__`, `mcp__QSVObmaRs4Fi18o8Z85wV__`,
`mcp__0HD-u86s20h3DuwbDikoX__` as of 2026-07-20). Do not hardcode or trust a prefix from memory/past
runs. Before the first Jiandaoyun call each run:
1. Check the current turn's system-reminder for a "deferred tools" list — it shows the live, correct
   prefix (look for `member_data_list`, `member_app_list`, etc.).
2. If no such reminder is visible, just try the call with the prefix noted in your working memory; if it
   errors `No such tool available`, that confirms a reconnect happened — re-check the system-reminder or
   ask the user to trigger one (their next message will carry the fresh deferred-tools list).
3. Never spin up a sub-agent just to "guess" the prefix by trial and error — the correct name is already
   surfaced passively via the system-reminder, no guessing needed.

Once you have the live prefix, all calls below use it (e.g. `<prefix>member_data_list`).

### Field names in 发货管理
| Label | widgetName |
|-------|-----------|
| 来源单号 (PO) | `_widget_1752480297833` |
| 合同ID | `_widget_1753080485366` |
| 提出人 | `_widget_1780972277486` (filter: `eq`, type `user`, value = **my JDY Username from the roster**; Mode 0 uses `in` with all enabled members' usernames) |
| 物流信息 (subform) | `_widget_1752565972448` (also exposed flattened as `info[]` with `sku_name`, `trackingno`, `logisticschannel_name`, `fororder_no`) |
| → 订单号 | `_widget_1752719174894` |
| → SKU | `_widget_1752565972450` |
| → SKU名称 | `_widget_1752718439218` |
| → 跟踪码 | `_widget_1752718439220` |
| → 物流渠道名称 | `_widget_1752718439221` |
| 物流信息是否完整 | `_widget_1752808960297` (`full`, "是"/"否") |
| updateTime (系统字段) | ship timestamp |

### Field names in 网红信息
| Label | widgetName |
|-------|-----------|
| 网红名称 | `_widget_1750747331717` |
| 联系邮箱 | `_widget_1751591561200` |
| 收件人姓名 | `_widget_1751246637939` |

### Field names in 合同管理
| Label | widgetName |
|-------|-----------|
| 合同ID | `_widget_1750748364947` |
| 网红ID | `_widget_1750748364948` |
| 网红名称 | `_widget_1750842205357` |
| 网红邮箱 Influencer Email | `influencer_email` (`_widget_1752132506399`) |

### Field names in 单个工单 (产品生命周期) — each member submits their own 催发货 here
| Label | widgetName |
|-------|-----------|
| RMAID (auto label, e.g. `RMA20260600411`) | `_widget_1774861977529` |
| 平台单号 | `oderid` (`_widget_1774861977532`) |
| 工单类型 | `type` (`_widget_1774861977533`) |
| 订单详情 subform | `detail` (`_widget_1774861977539`) |
| → 跟踪号 | `trackno` (`_widget_1774861977542`) |
| → 系统单号 | `fo_oder` (`_widget_1774861977541`) |
| → SKU名称 | `skuname` (`_widget_1778661643449`) |
| → 订单状态 | `status` (`_widget_1778830939654`) |
| → 是否有异常 | `error` (`_widget_1778565076447`) |
| flowState (系统) | `flowState` — 已完成 = processed |

### Field names in 批量工单 (产品生命周期) — same as 单个工单 but for multiple POs
| Label | widgetName |
|-------|-----------|
| 平台订单号 (subform, per row) | `_widget_1778833451783` |
| 跟踪号 | `_widget_1774861977542` |
| SKU名称 | `_widget_1778661643449` |
| 是否有异常 | `_widget_1778565076447` |

### Field names in 执行工单 (产品生命周期) — warehouse processes each item, tracking lands here
| Label | widgetName |
|-------|-----------|
| RmaId | `_widget_1776219080908` |
| 平台单号 | `source_order_no` (`_widget_1774860170329`) — this is the JDY internal order number, **not** the PO name |
| 工单类型 | `type` (`_widget_1774860170327`) |
| 跟踪号 | `_widget_1780391793600` |
| 新--跟踪号 (post-换货/补发) | `new_track` (`_widget_1780391793602`) |
| 系统单号 | `xitong_no` (`_widget_1776219111186`) |
| SKU名称 | `skuname` (`_widget_1778662217862`) |
| 处理结果 | `result` (`_widget_1774860170334`, combo — 待处理/已完成 etc.) |
| 是否有异常 | `status` (`_widget_1778651173620`) |
| 异常详情 | `error` (`_widget_1782093678117`) |

⚠️ Correct two-step lookup for 执行工单 (since it's keyed by RmaId, not PO name):
1. Query **单个工单** filtered on `oderid` = PO name → extract the record's label (RMAID, e.g. `RMA20260600411`)
2. Query **执行工单** filtered on `_widget_1776219080908` (RmaId) = that RMA ID

---

## Base Config (Feishu — department-shared, built 2026-08-11)

| Item | Value |
|------|-------|
| Base name | RITFIT Shipping (部门版) |
| Base token | `UGuHbNi6GapDwTs9kdUcUGZPnzS` |
| Table name | Shipment Tracking |
| Table ID | `tblSbLstfA1xz5M4` |
| Existing view | "Shipment View" (`vewrSfNvpO`) — grid, no saved filter; query with an explicit `filter` instead of relying on this view |
| Roster table | 部门成员 `tblfDTCs6XeMPefA` (姓名 / JDY Username / Feishu Open ID / 企业邮箱 / 启用) |

**⚠️ Advanced permissions are ON (2026-08-11).** The system `Editor` role (rolUbyYp27y) is
deliberately narrowed: members can edit **only** `Tracking Info` / `Carrier` / `Email Sent?`,
cannot add/delete records, and can only edit rows where `负责人` (user field) = themselves;
everyone can read all rows; the roster table is read-only for members. The department admin
(currently 吴双) is the Base owner and unrestricted. Consequences for this skill:
- **Mode 0 (central, runs as the admin's identity)** is the only writer of new rows and of
  `Status` / `All Shipped?` — members' instances must not attempt those writes (they'd 403).
- **Mode B (member's own instance)** may write `Tracking Info` / `Carrier` (B2.5/B2.6) and
  `Email Sent?` (B3.5) **only on rows where 负责人 = me**. If a write returns a permission error,
  don't retry-loop: note it in the daily-log and let central Mode 0 catch it on its next run.
- When Mode 0 writes a row it must set both `Owner` (text, 提出人姓名) and `负责人`
  (user field, `[{"id":"<open_id>"}]` from the roster) — without `负责人` the row is invisible
  to the owner's own edit permission.

**⚠️ Legacy note:** the pre-department personal table (RITFIT Brand Base `Dj0BbQHGKaiaFssqeWucIBIen8g`,
table `tblWl75PwJmouKlU`) is frozen as of 2026-08-11 — kept for history, never written by this
skill anymore. All reads/writes below target the department Base above.

All fields already exist — **do not create new fields**.

| Field | Type | Notes |
|-------|------|-------|
| ID | auto_number | genuine primary field (first field, created first) — system-managed, ignore for day-to-day writes |
| PO Number | text | 来源单号 |
| KOL Name | text | extracted from PO Number, see rule below |
| SKU | text | 中文产品名 |
| Tracking Info | text | leave blank if JDY has none |
| Carrier | text | |
| Ship Date | datetime (yyyy-MM-dd HH:mm) | |
| Status | single-select: `In Progress` / `Completed` / `Not in JDY` / `Flagged` | |
| All Shipped? | checkbox | true when `full`="是" or watchlist check confirms every item tracked |
| Email Sent? | checkbox | `true` only after the KOL tracking email has actually been **sent** (never for draft-only). See B3.5 step 6. |
| Owner | text | 提出人姓名 (e.g. "吴双", "Diego Isaac Rojo Molina") — from roster, never hardcoded |
| 负责人 | user (single) | the owner's Feishu user `[{"id":"ou_xxx"}]` — powers the "edit own rows only" permission |
| 发货单号 | text | JDY 发货单 SN |
| 唯一键 | text | `发货单号||订单号(fororder_no)` — use order number, not tracking number, so blank-tracking rows still dedup correctly. ship-单号 is globally unique across members, so no owner prefix is needed |

---

## File structure (per-member, inside the current agent's own workspace)

All paths below are **relative to the current agent's workspace root** (resolve it at runtime —
on 吴双's machine the workspace is `F:\Cherry Studio\Andy\Data`, but each member's published-agent
workspace differs; never hardcode another machine's absolute path):

| Relative path | Purpose |
|------|---------|
| `_outputs\ritfit-tracking-JDY-outputs\watchlist.md` | Active POs to monitor — PO name + ship date only |
| `_outputs\ritfit-tracking-JDY-outputs\completed.md` | Permanent log of fully-tracked POs — full tables, never deleted |
| `_outputs\ritfit-tracking-JDY-outputs\daily-log-DD-MM-YYYY.md` | One file per daily run, e.g. `daily-log-19-06-2026.md` |
| `_outputs\ritfit-tracking-JDY-outputs\mode0-state.md` | Mode 0's incremental cursor — one line, `last_run_at: <ISO timestamp>` — **central machine only** |
| `<this skill's dir>\references\tracking.html` | KOL tracking-email HTML template (`{{KOL_NAME}}` / `{{TRACKING_TABLE}}` placeholders) — bundled with the skill, do not recreate |

---

## Mode 0 — Daily incremental sync (JDY → Base) — central admin machine only

Runs via cron "JDY-Base 每日自动同步" (`30 1,14 * * *` — 2x/day at 01:30 and 14:30 BJT) on the
central admin machine (currently 吴双's). **Members' own instances never run Mode 0** — their
environment only runs Mode B.

**⚠️ v4.3.0 (2026-07-20): converted from full historical rescan to incremental.** The old version
re-queried and re-dedup-checked *every* shipment ever submitted, every single day. That was
fine when JDY only had a handful of records, but by 2026-07-20 there were 100+ shipments / 988+ Base
rows and it started timing out at the 15-minute cron limit (job got auto-`paused`). Root cause: O(all
history) work repeated daily instead of only processing what changed. Fixed by adding a state file and
filtering on `updateTime`, the same incremental pattern used by Mode B and `jdy-kol-sync`.

**⚠️ v5.0.0 (2026-08-11): department-wide.** Mode 0 now syncs shipments for **every enabled roster
member**, not just the admin, and writes `Owner` (提出人姓名) + `负责人` (their Feishu user) on every
row so per-member permissions work. It only picks up shipments created/updated from 2026-08-11
onward — historical shipments of other members are never backfilled (per 吴双's decision).

State file: `_outputs\ritfit-tracking-JDY-outputs\mode0-state.md` (central machine only)
— one line: `last_run_at: <ISO timestamp>`.

**⚠️ v4.8.0 (2026-08-07): added 执行工单 tracking-number supplementation (Mode 0 counterpart to
Mode B's B2.6).** JDY 发货管理's `trackingno` often requires a manual 查询并刷新 to
appear — so a 发货/换货 work order can already have a real tracking number in 执行工单 (`new_track` /
跟踪号) while 发货管理 is blank. B2.6 already covers watchlist POs in Mode B; v4.8.0 extends the
same idea to Mode 0's incremental sync so **every** shipment in the window gets its blank tracking
filled from 执行工单 — not just watchlist POs. It runs as a new step 9b after the main 发货管理 sync,
before updating `mode0-state.md`.

Logic:

1. Read `mode0-state.md` for `last_run_at`. If the file is missing, bootstrap: set `last_run_at` to
   72 hours before now (generous overlap — safe because step 5's dedup will skip anything already in
   Base) and skip straight to step 8.
2. Record `run_start = now (ISO)` — this becomes the new `last_run_at` at the end of the run, **not**
   the time the run finishes (so shipments submitted mid-run next time still get picked up next
   time).
3. Read the roster (部门成员 table) and collect `usernames = [JDY Username of every row where
   启用=true and JDY Username non-empty]`. Then query `<current Jiandaoyun MCP prefix>member_data_list`
   on 发货管理, filter: `提出人 ∈ usernames` AND `updateTime >= last_run_at`
   (see the ⚠️ prefix-instability note in Jiandaoyun Config above — resolve the live prefix first)
   ```json
   {"cond":[
     {"field":"_widget_1780972277486","method":"in","type":"user","value":["<username1>","<username2>",...]},
     {"field":"updateTime","method":"gte","type":"datetime","value":["<last_run_at>"]}
   ]}
   ```
   **⚠️ Must use `gte`, never `gt` — verified 2026-07-20.** Jiandaoyun's `datetime` field type only
   supports `eq/ne/gte/lte/range/empty/not_empty` as filter methods; `gt`/`lt` are not supported for
   datetime fields (`gt` is silently ignored by the API rather than erroring — the query just returns
   everything unfiltered, which is exactly how the original full-rescan bug hid inside what looked
   like a working incremental filter during testing). Always include `"type":"datetime"` on the
   condition too. Value format: plain `"YYYY-MM-DD"`, `"YYYY-MM-DD HH:mm:ss"`, or full ISO
   `"YYYY-MM-DDTHH:mm:ssZ"` all parse correctly (verified) — pick one and store `last_run_at` in
   `mode0-state.md` in that same format so it round-trips without reformatting.

   Use `updateTime`, not `createTime` — a shipment created days ago whose tracking number just got
   filled in (`full` flips 否→是) touches `updateTime`, not `createTime`. Filtering on `createTime`
   would silently miss those updates forever. Paginate with `limit=100` + `cursor_data_id` (should
   now be at most one page per day in normal operation).
4. For every shipment returned, iterate **all** subform items (`info[]`) regardless of tracking
   presence. Read the shipment's 提出人 (`_widget_1780972277486`) → look up the roster row by
   username → that row's 姓名 becomes `Owner`, its open_id becomes `负责人`. If the 提出人 isn't in
   the roster (e.g. someone outside the brand dept, or a roster row with 启用=false) → skip the
   whole shipment silently (log a one-line note).
5. Map fields per the Base Config table above. `full`="是" → `Status=Completed`, `All Shipped?=true`;
   `full`="否" → `Status=In Progress`, `All Shipped?=false`.
6. Extract **KOL Name** from PO Number (see extraction rule below).
7. Dedup: before writing, `lark-cli base +record-search --search-field 唯一键 --keyword "<发货单号>||<订单号>"`.
   Skip if found (this also covers the bootstrap-overlap and any edge case where the same shipment
   updates twice in one window).
8. Write with `lark-cli base +record-upsert --base-token UGuHbNi6GapDwTs9kdUcUGZPnzS --table-id tblSbLstfA1xz5M4 --json '{...}' --as user`.
   The JSON must include `"Owner": "<提出人姓名>"` and `"负责人": [{"id": "<open_id>"}]` (user-field
   cell format) — without `负责人` the row's owner can't edit their own tracking fields later.
9. Serial writes, 0.5–1s between every 3 records. Log errors, keep going — never abort the run.
9b. **执行工单追踪号补充同步 (v4.8.0, 2026-08-07; department-wide since v5.0.0).** 发货管理 `trackingno`
    常需手动"查询并刷新"才显示，但追踪号可能已真实落地在**执行工单**（`new_track`/跟踪号）。本步骤把执行工单
    当作追踪号的权威补充来源，把窗口内空单号的行从执行工单补上。只在主流程（步骤 3-9）之后执行，不替代主流程。
10. Update `mode0-state.md` to `last_run_at: <run_start>` (the timestamp captured in step 2).
11. Report to the admin (via `mcp__claw__notify`, only if anything changed — no notify on a no-op
    run): how many shipments were in the incremental window (broken down by Owner), how many rows
    written/updated, how many skipped as duplicates.

### Mode 0 step 9b — 执行工单追踪号补充同步 (v4.8.0, 2026-08-07; department-wide v5.0.0)

**目的**：发货管理 `trackingno` 常需手动"查询并刷新"才显示，但追踪号可能已真实落地在**执行工单**
（`new_track` 换货/补发新单号，或原始跟踪号 `_widget_1780391793600`）。本步骤把执行工单当作追踪号的权威
补充来源，把主流程步骤 3-9 里 trackingno 仍为空的 Base 行补上单号。**触发时机**：每次 Mode 0 主流程
（发货管理增量同步）完成后、更新 state 之前，对窗口内所有发货单执行。

前置（B2 同款两步查询）：
1. 对窗口内每个发货单，用来源单号（`_widget_1752480297833`）查**单个工单**（entry `69ca3e985befcf33adf37ae6`，
   filter `_widget_1774861977532`(oderid) like 来源单号 **AND `_widget_1774861977528`(提交人) eq 该发货单的提出人
   username**——即主流程步骤 4 里为该发货单解析出的同一个人），拿 RMA ID。
   - ⚠️ **单个工单表的提出人字段是 `_widget_1774861977528`（label=提交人）**，不要跟执行工单表的
     `_widget_1774860170326`（label=提出人）搞混——两张表字段名不同！
   - ⚠️ **必须按该发货单自己的提出人过滤**（v5.0 起不再是固定某一个人）：工单表里混着全部门同事发起的记录。
     只有提出人在名册（启用=true）内的发货单才会进入窗口（主流程步骤 3 已保证），所以这里的过滤值就是
     该发货单的提出人 username。**通过 Andy (influencer-exchange-order) 创建的工单提交人=实际操作的那个
     成员自己（模板显式填其 username），会正常命中**。
   - ⚠️ 但注意：发货管理来源单号可能带后缀（如 "Daniel O'Connor-换3"），单个工单 oderid 存的可能是原始
     PO（"Daniel O'Connor"）。所以查单个工单时用 `like` 而不是 `eq`，且要把执行工单的 `source_order_no`
     也作为候选 PO 名一起匹配（见步骤 3 的匹配策略）。
2. 查**执行工单**（entry `69ca3653e80a04d0ebe03a38`，filter `_widget_1776219080908`(RmaId) = RMA ID
   **AND `_widget_1774860170326`(提出人) eq 该发货单的提出人 username**，limit=100 分页），返回该工单所有行。
   ⚠️ 执行工单表的提出人字段是 `_widget_1774860170326`（user 类型），必须过滤为该发货单自己的提出人，
   防止匹配到其他同事的同名/相似 RMA。

对执行工单返回的**每一行**：
1. **取追踪号**：优先 `new_track`（`_widget_1780391793602`）；为空则取原始跟踪号 `_widget_1780391793600`。
   两者都为空 → 该行无单号，跳过。
2. **只认已完成的行**：`result`（`_widget_1774860170334`）必须是 `已完成(Done)` 才采信。待处理/部分完成/
   有异常 → 跳过（追踪号是中间态，等下一轮）。
3. **⚠️ 异常检查**：若 `status`（`_widget_1778651173620`，是否有异常）非空或 `error`（`_widget_1782093678117`）
   非空，即使有 new_track 也**不写入**，把告警带出给管理员（写进 daily-log 的 ⚠️ 行），不要静默写入。
4. **匹配 Base 行**：用 PO Number + SKU 中文名找 Base 行：
   - 候选 PO 名列表：发货管理来源单号、执行工单 `source_order_no`（`_widget_1774860170329`）、
     `after_no`（`_widget_1778651173621`，新--平台单号，如 "Daniel O'Connor-X-RE-01"）——**逐个去试**，
     因为换货工单下这三个值可能都不同（实测 Daniel O'Connor-换3：发货管理="Daniel O'Connor-换3"、
     单个工单 oderid="Daniel O'Connor-换3"、执行工单 source_order_no="Daniel O'Connor"、
     after_no="Daniel O'Connor-X-RE-01"）。
   - 对每个候选 PO 名，`lark-cli base +record-search --search-field "PO Number" --keyword "<候选PO>" --as user`
     拿回该 PO 全部 Base 行；
   - 在结果里用 SKU 文本精确比对（trim 后完全相等）。优先比 `after_skuname`（`_widget_1778662217863`，
     换货后SKU名），再比 `skuname`（`_widget_1778662217862`），再比发货管理 info[].sku_name。命中 → 拿
     `record_id`。
   - **找不到匹配的 Base 行**（PO 从未进 Base，或 SKU 名对不上）→ 跳过，记入 daily-log 备注（"执行工单有
     单号但 Base 无对应行：PO=<x> SKU=<y>"）。这通常意味着该 PO 还没被发货管理/主流程同步过，Mode 0 会在
     后续窗口里通过发货管理建行；不要为了补单号而凭空建行。
5. **写入 Base**：命中后 `+record-upsert --record-id "<rec_id>" --json '{"Tracking Info": "<追踪号>", "Carrier": "<carrier>"}' --as user`。
   - `Carrier`：**执行工单不存物流渠道**。优先取发货管理该行已有的 `logisticschannel_name`（原始值不
     normalize）；发货管理里也没有 → 留空（不编造）。若按单号格式能猜出承运商大类（1Z→UPS、纯数字→
     FedEx/DHL）可在日志/通知里标注"（from tracking format）"供该 PO 的 Owner 确认，但 Base 的 Carrier 字段只写
     确切的来源值或留空。
   - 只动 `Tracking Info` / `Carrier`，不碰 `Status`/`All Shipped?`（B3/B4 职责）。
6. **幂等/去重**：Base 行 `Tracking Info` 已等于要写的值 → 跳过（no-op）。同一 RMA 多行共用同一
   `new_track`（几箱一个包裹）→ 逐行各写一遍，不加序号。若主流程步骤 3-9 已经写了该行且 trackingno 非空，
   本步骤会 no-op 跳过，不重复。
7. **日志**：daily-log 记录 "9b 执行工单补写 N 个追踪号（PO: …）"；如果本次窗口没有任何需要补写的
   （多数情况下执行工单单号已同步或发货管理已有 trackingno），记一行 "9b 无待补写" 即可，不强制通知。

⚠️ **数据量控制 + 提出人过滤（双重保险）**：执行工单表是全量工单历史，不能每次全扫。本步骤**只对主流程
窗口内返回的发货单**（提出人 ∈ 启用名册且 updateTime≥last_run_at）执行工单两步查询——也就是说，一个发货单如果
没有进入 Mode 0 增量窗口（updateTime 没变），本步骤不会主动去查它的工单。这是因为 Mode 0 的窗口设计就是
"只处理变化"；补写只在窗口内发货单的 trackingno 为空时触发。若某 PO 在窗口外但有新落地的执行工单单号，那
属于 Mode B（watchlist PO）或下一个窗口的范畴。

⚠️ **⚠️ 提出人必须在启用名册内是硬性前置条件**：即使某 PO 的候选名匹配到了执行工单，也必须在**单个工单
查询**和**执行工单查询**两步都按该发货单自己的提出人 username 过滤（主流程步骤 3 已保证窗口内只会有名册
内成员的发货单）。**名册外人员发起的工单即使 new_track 已落地，也不补写、不建行**——每个成员只给自己
负责的网红发物流邮件，Base 只收录名册内成员的 PO。

### KOL Name extraction rule
Strip trailing markers from PO Number:
- Date suffix: `(26-07-17)` → strip
- Restock marker: `补` → strip
- Series marker: `G-PRO`, `GPRO` → strip
- Numeric suffix: `-2`, `-4`, `-7` → strip

| PO Number | KOL Name |
|---|---|
| Mike Holston 补 | Mike Holston |
| Mike Holston G-PRO | Mike Holston |
| Jeff Seid-4 | Jeff Seid |
| Yilda Garcia (26-07-17) | Yilda Garcia |
| fit.w.ash_ | fit.w.ash_ |

### 初始化 / 清空 (reset Base) — 管理员操作
```
lark-cli base +record-list --base-token UGuHbNi6GapDwTs9kdUcUGZPnzS --table-id tblSbLstfA1xz5M4 --limit 200 --field-id "ID" --format json --as user
lark-cli base +record-delete --base-token UGuHbNi6GapDwTs9kdUcUGZPnzS --table-id tblSbLstfA1xz5M4 --json '{"record_id_list":["rec_xxx",...]}' --yes --as user
```
(batch ≤50 per delete call). Historical JDY data is never backfilled — only what syncs going forward.

---

## Mode A — Add PO to watchlist

Triggered when the current member gives a 来源单号 to start tracking.

1. **Collect inputs**: PO name (e.g. `showroom_0618`) + ship date (ask if missing).
2. **Confirm in JDY**: query 发货管理 filtered on `_widget_1752480297833` = PO name AND 提出人 =
   the current member (from identity resolution). If the record exists but belongs to someone else,
   warn the member instead of adding it to their watchlist.
3. **Append to watchlist.md**: `| showroom_0618 | 2026-06-18 |`
4. **Confirm**: "Added showroom_0618 to the watchlist (ship date: 2026-06-18). I'll check JDY daily and notify you when all tracking is filled."
5. **On first use only**, remind: "To run the daily check automatically, say 'schedule daily tracking check' and I'll set it up with `mcp__claw__cron`." (On the central admin machine this is already scheduled — see Scheduling section — so this reminder is mainly for newly-onboarded members.)

### Mode A variant — Update watchlist from Base

Triggered by "update watchlist" / "sync watchlist" / "refresh watchlist" / "pull watchlist from Base".

1. Query Base `tblSbLstfA1xz5M4` with filter `All Shipped? = false` AND `PO Number not empty`
   AND `Owner = <my 姓名>` (no need for a saved view — pass `filter.cond` on the query call).
2. Extract per row: PO Number, Ship Date, KOL Name, SKU (context only).
3. Read `watchlist.md`; for each PO from Base — skip if already present, else append.
4. Confirm: list what was added, or "Watchlist is already up to date."

### Mode A variant — Auto-added by influencer-exchange-order (2026-07-22)

The `influencer-exchange-order` skill (创建网红换货/催发货/新增订单工单) writes directly to the
current member's own `watchlist.md` after it successfully creates a 新增订单/换货/催发货 work order
in JDY — this is by design, not a bug: members asked for this so they don't have to manually forward
newly-created POs. No action needed on jdy-shipping-track's side; just be aware that watchlist
entries can originate from that skill, not only from Mode A / Mode A variant above. The 发货管理
record for these POs is itself auto-generated by the JDY workflow ~10s after work-order submission
(提出人 = the member who ran influencer-exchange-order), so Mode 0's normal incremental scan will
also pick it up independently.


---

## Mode B — Daily check (per-member)

Triggered on schedule (each member's own cron, e.g. "Daily shipping tracking check") or
"check open POs" / "check watchlist". **Scope is always the current member**: after identity
resolution, every JDY/Base query in Mode B filters to 提出人/Owner = me. If a watchlist PO turns
out to belong to someone else, skip it and note it in the daily-log.

**⚠️ v4.5.0 (2026-07-20):** added B2.5 — live tracking sync. Previously Mode B only wrote to Base
when a PO fully completed (B3) or got flagged (B4); a PO that was still in-progress but had picked
up a fresh tracking number mid-day stayed blank in Base until Mode 0's nightly `0 1 * * *` run.
吴双 asked for that gap closed (noticed a fresh tracking number missing from Base right after a
14:00 check), so every Mode B run now pushes any newly-appeared tracking numbers straight to Base
as it finds them — Mode 0 is unchanged and still runs nightly as the full incremental
system-of-record sync; B2.5 is just a same-day mirror for watchlist POs specifically.

**⚠️ v4.7.0 (2026-08-07):** Added **B2.6 — 执行工单追踪号补充同步**. JDY 发货管理's
`trackingno` often does NOT auto-refresh — the member has to manually click 查询并刷新 before the tracking
number shows up in 发货管理. That means a 发货/换货 work order can already have a real tracking number
in 执行工单 (new_track / 跟踪号) while 发货管理's trackingno is still blank → Base stays blank → B3
judges the PO "missing tracking" → the KOL email can't be sent. B2.6 fixes this by treating 执行工单
as an authoritative **supplementary tracking source**: when 发货管理 has no tracking for a line but the
PO's 执行工单 has a landing new_track/跟踪号 with result=已完成(Done), B2.6 writes that tracking number
straight into Base (matched by PO Number + SKU 中文名). This directly addresses the "已发货/已换货但发货管理
不显示单号 → 无法及时更新 Base" problem 吴双 raised on 2026-08-07.

**⚠️ v4.6.0 (2026-07-21):** B3.5 step 1 gained a fallback — if 网红信息/合同管理/Base ALL KOLs all
come up empty for a KOL's email, search the current member's own mailbox by KOL name
(`lark-cli mail +triage --mailbox me --query "<KOL name>"`) before giving up. Verified working on
Alex Siquig - PLC01 (found `siquigalexander@gmail.com` from an existing thread). Also documented
that `lark-cli mail +send` cannot create a draft with zero recipients — when no email is found even
after this fallback, save the assembled HTML to a tmp file instead of attempting a no-recipient draft.

**⚠️ v4.6.1 (2026-07-21):** Diagnosed a Mode 0 timeout on 2026-07-21 — root cause was NOT data volume
(manually re-ran the exact incremental query: only 5 shipments / 34 rows in the window, trivially
fast), it looked like a transient infra/session hiccup shared by multiple unrelated cron jobs firing
in the same ~08:56-09:15 BJT window that morning. Manually caught up the missed sync. While doing so,
discovered a real dedup edge case (see B2.5 step 2 above): JDY can regenerate a subform item's
`fororder_no` once it actually ships, silently breaking 唯一键-based lookups for that item even though
its Base row already exists — added a PO-Number+SKU fallback so B2.5 updates the existing row by
`--record-id` instead of leaving it stuck blank (or worse, creating a duplicate).

**⚠️ v4.6.2 (2026-07-21):** Clarified B2's "no record / empty subform / `full` error" branch (see
above) — this is much more often a JDY propagation delay (单个新增 → 发货管理 submitted back-to-back)
than an actual data-entry error. Verified on Jarell Carter - 奥杆: flagged as a data problem needing
the member's attention on the 09:00 run, came back clean with real subform data on manual re-check the same
day. Don't tell the member to go fix it in JDY until it's stayed empty/erroring across 2+ Mode B cycles.

**⚠️ v4.6.3 (2026-07-21):** Mode 0 bumped from 1x/day (`0 1 * * *`, 01:00 BJT) to 2x/day
(`30 1,14 * * *`, 01:30 and 14:30 BJT) per 吴双's request, to shrink the max data-staleness window
from 24h to ~13h. Job re-created (new id, old one removed — the cron tool has no update action).
The `:30` offset is deliberate, to avoid the exact on-the-hour minute used by Mode B / RITFIT Email
Reply's cron slots (helps avoid a repeat of the 2026-07-21 morning clustering incident, even though
that was judged a one-off).

**B1 — Read watchlist.** If empty: print "Watchlist is empty. Nothing to check." and stop.

**B2 — Query JDY per PO and render.**

Query 发货管理 filtered on `_widget_1752480297833` = PO name AND `_widget_1780972277486`(提出人) =
my JDY Username (from identity resolution).

- **No record found, OR record found but subform empty / `full` field returns an error like "请输入正确的平台单号"** →
  render `## [PO name]  |  ⏳ Not in JDY yet`, skip 3-day flag, move to next PO. **Do not conclude this
  is a JDY data-entry error requiring the member to fix it.** As of 2026-07-21 (Jarell Carter - 奥杆 case),
  the far more common cause is a propagation delay: the member submitted 发货管理 immediately after doing a
  "单个新增" for the same PO, and JDY's backend needs some time (observed: well under a day) to sync
  the new-item data through to 发货管理. Just re-check on the next scheduled Mode B run — don't flag
  it to the member as something they need to go fix unless it's still empty/erroring after 2+ check cycles.
- **Record found** → extract from subform: SKU名称 (translate to English, per the rule below), 跟踪码,
  物流渠道名称 (normalize per the Carrier rule below).

### SKU English translation rule — strip factory/OEM names (吴双 flagged 2026-07-22, department-wide rule)

SKU中文名里常带代工厂名（拼音），不是官网展示的产品名——KOL看到会困惑，容易以为品牌/产品名变了。
案例："锦华款三段式哑铃凳黑1.1版" 曾被直译成 "**Jinhua** 3-Position Adjustable Dumbbell Bench,
Black v1.1"（Jinhua=锦华，代工厂名），发给KOL Alex Siquig的追踪邮件里出现了这个问题。

**规则（全员适用）：翻译SKU英文名时，任何代工厂拼音一律省略不译，只保留产品本身的英文描述。**

已知的代工厂拼音黑名单（遇到就从翻译结果里去掉，不要出现在任何面向KOL/外部的英文文本里）：
- `锦华` → Jinhua（已确认案例）
- `创源` → Chuangyuan（已确认案例，出现在"M2 Weight Plate Flag Sticker — Chuangyuan Edition"里）

这份黑名单不是穷尽列表——翻译时如果遇到任何"看起来像代工厂名的拼音"（通常是SKU名开头的品牌感修饰词，
且不对应RITFIT官网/产品目录里的已知产品线名），都应该本着这条规则省略，不要因为不在上面两个词里就直译。
如果拿不准某个词是不是工厂名，优先省略，而不是照抄拼音。

### Carrier normalization rule (for all English-facing outputs)

`物流渠道名称` from JDY is inconsistent — same physical carrier shows up under different labels
depending on service tier / domestic forwarder (e.g. `Fedex-环世快递`, `FEDEX_GROUND`,
`FEDEX_HOME_DELIVERY`, `FEDEX_ECONOMY`, `US-FedEx Ground`). Showing several different-looking
FedEx labels in the same KOL email is confusing (吴双 flagged this 2026-07-17) — KOLs only need to
know which carrier network to track on, not the service tier or domestic broker. Rule:

- **Mode 0 (Base sync)**: write the raw JDY value unchanged into the Base `Carrier` field — Base is
  the internal system of record and the service-tier/broker detail has ops value there.
- **Mode B outputs (SKU tables in daily-log/completed.md, and anything sent to a KOL)**: normalize
  by carrier family, case-insensitive substring match on the raw value:
  - Contains `fedex` → `FEDEX` (collapses `Fedex-环世快递`, `FEDEX_GROUND`, `FEDEX_HOME_DELIVERY`,
    `FEDEX_ECONOMY`, `US-FedEx Ground`, etc. — any service tier or forwarder — into one label)
  - Contains `ups` → `UPS`
  - Contains `dhl` → `DHL`
  - Contains `usps` → `USPS`
  - Anything else (e.g. `GOFO GROUND`, `Amazon_Ground`) → keep as-is, just strip any trailing CJK
    text after a `-` if present
  Example: `Fedex-环世快递` → `FEDEX`; `FEDEX_HOME_DELIVERY` → `FEDEX`; `GOFO GROUND` → `GOFO GROUND`.

Also check **单个工单 + 批量工单** for escalation status: query filtered on 平台单号 = PO name, type = 催发货.
If found, note RMA ID + flowState (已完成 = processed) — do not re-flag a 3-day alert if a work order already exists.

Also check **执行工单** via the two-step RmaId lookup (see Jiandaoyun Config section above). If it has
tracking for items still blank in 发货管理:
- Use that tracking (authoritative — it comes from the 催发货 escalation pipeline)
- Add `📋 RMA: [RmaId]` next to it
- If 是否有异常/异常详情 is non-empty, flag it: ⚠️ Warehouse error — surface the text to the member
- **v4.7.0 (2026-08-07):** this is now more than a render-only note — if the tracking landed in
  执行工单 but 发货管理's trackingno is still blank, **B2.6 writes it straight into Base** (see B2.6
  below) instead of just showing it in the daily log. The render keeps showing the RMA tag for the log
  / notification, but the Base write is handled by B2.6.

Render:
```
## showroom_0618  |  Ship date: 2026-06-18
| SKU Name | Tracking No. | Shipping Method |
|----------|-------------|-----------------|
| MUSTANG Leg Extension Machine Box A | 873237155661 | FEDEX_GROUND |
| MUSTANG Leg Extension Machine Box B |  | FEDEX_GROUND |
```

**B2.5 — Live tracking sync to Base (added v4.5.0, 2026-07-20).** Don't wait for tonight's Mode 0
run to mirror newly-appeared tracking numbers — write them to Base immediately, every time Mode B
checks a PO. This runs for **every** watchlist PO on every check, not just completed/flagged ones.
Mode 0's nightly `0 1 * * *` incremental sync is unaffected and keeps running exactly as before —
this step is just a faster mirror scoped to watchlist POs; POs not on the watchlist still only get
synced by Mode 0.

For each item in the JDY `info[]` subform for this PO that has a non-empty `trackingno`:
1. Look up the matching Base row by its dedup key (same key Mode 0 uses):
   `lark-cli base +record-search --base-token UGuHbNi6GapDwTs9kdUcUGZPnzS --table-id tblSbLstfA1xz5M4
   --search-field 唯一键 --keyword "<发货单号>||<fororder_no>" --as user`. This is a substring
   search — verify the returned row's 唯一键 is an exact match (trim-compare) before using it.
2. **Not found by 唯一键** → before concluding "not synced yet", try one fallback (added 2026-07-21,
   confirmed real on Kody Workman 哑铃): search Base by **PO Number** instead
   (`+record-search --search-field "PO Number" --keyword "<PO name>"`) and match by SKU text. JDY can
   **regenerate `fororder_no` for a subform item once it actually ships** — an item that sat blank in
   Base with an old `fororder_no`-based 唯一键 can silently get a *new* `fororder_no` in JDY the moment
   its tracking number appears, so the 唯一键 search for the new value will never find the old row even
   though it's sitting right there in Base waiting to be filled in. If a PO Number+SKU match finds an
   existing blank/stale row this way, update that row **by its `--record-id`** directly rather than
   creating a new one — do not blindly upsert-by-唯一键, which would create a duplicate row and leave
   the original permanently blank. Only if PO Number search also finds nothing is it genuinely
   "not synced yet" (e.g. a showroom PO) → skip silently, Mode 0 will create the row later.
3. **Found, and Base's `Tracking Info` already equals the JDY value** → skip, no-op (avoid
   redundant writes on every check).
4. **Found, and Base's `Tracking Info` is blank or different** → write it now:
   `lark-cli base +record-upsert --base-token UGuHbNi6GapDwTs9kdUcUGZPnzS --table-id tblSbLstfA1xz5M4
   --record-id "<rec_id>" --json '{"Tracking Info": "<trackingno>", "Carrier": "<raw
   logisticschannel_name>"}' --as user`. Carrier is written **raw** (unnormalized), matching the
   Mode 0 rule below — Base is the internal system of record and keeps the service-tier/broker
   detail; only Mode B's rendered/emailed *output* uses the normalized carrier name.
   ⚠️ Permission note (v5.0): this write only succeeds on rows where 负责人 = me (Base advanced
   permissions). If it fails with a permission error, log it and move on — central Mode 0 will
   catch the value on its next run.
5. This step only ever touches `Tracking Info` / `Carrier`. It never sets `Status` or
   `All Shipped?` — that stays B3's (completion) and B4's (flag) job, which still run right after
   using this now-current data. Since writes are by `--record-id` (not a fresh upsert-by-key), this
   can never create a duplicate row or race with tonight's Mode 0 run — Mode 0 will just re-write
   the same value later and no-op.
6. Note in the daily-log run entry if any live syncs happened this run (e.g. "Live-synced 2
   tracking numbers to Base ahead of tonight's Mode 0 run").

**B2.6 — 执行工单追踪号补充同步 (added v4.7.0, 2026-08-07).** 发货管理 `trackingno` 常需手动"查询并刷新"才显示，
但追踪号往往已经真实落地在**执行工单**（`new_track` 换货/补发新单号，或 `_widget_1780391793600` 原始跟踪号）。
本节把执行工单当作追踪号的权威补充来源，凡是发货管理没有单号、但执行工单已完成的，直接写进 Base。
**只在 B2 发现"发货管理某行 trackingno 为空"时对那一行触发**（发货管理已有单号的行走 B2.5 逻辑，不要重复处理）。
（权限同 B2.5：只能写 负责人=我 的行；失败就留给集中 Mode 0 兜底。）

前置：B2 已经做过 单个工单→RMA ID→执行工单 两步查询（见 Jiandaoyun Config 的执行工单查找说明）。
本节直接复用该结果。若 B2 因某种原因没查执行工单，先补查：
```json
{"cond":[{"field":"_widget_1776219080908","method":"eq","value":["<RMA ID>"]}]}
```
（app_id `66d696158e78f315b2476b1b`, entry_id `69ca3653e80a04d0ebe03a38`, 分页 limit=100。）

对执行工单返回的**每一行**：
1. **取追踪号**：优先 `new_track`（`_widget_1780391793602`，换货/补发后的新单号）；为空则取原始跟踪号
   `_widget_1780391793600`；两者都为空 → 该行无单号，跳过。
2. **只认已完成的行**：`result`（`_widget_1774860170334`）必须是 `已完成(Done)` 才采信。若 result 不是
   已完成（待处理/部分完成/有问题），跳过该行——追踪号可能是中间态，等下一轮。⚠️ 若 `status`（`_widget_1778651173620`，
   是否有异常）非空或 `error`（`_widget_1782093678117`）非空，即使有 new_track 也**不写入**，按 B2 的
   异常规则把告警带出给该成员（写进 daily-log 的 ⚠️ 行），不要静默写入。
3. **匹配 Base 行**：该执行工单行有 `skuname`（`_widget_1778662217862`，原始SKU名）和
   `after_skuname`（`_widget_1778662217863`，换货后的SKU名）。用 PO Number + SKU 中文名找 Base 行：
   - 优先 `lark-cli base +record-search --search-field "PO Number" --keyword "<PO name>" --as user`，
     拿回该 PO 全部 Base 行；
   - 在结果里用 SKU 文本精确比对（trim 后完全相等）。优先比 `after_skuname`，再比 `skuname`，再比原始
     发货管理里该行的 sku_name。命中 → 拿到 `record_id`。
   - 若 Base 里没有该 PO 的任何行（例如 showroom PO 或空壳单从未进过 Base）→ 按 B2.5 step 2 的规则静默跳过，
     留待 Mode 0 建行（本功能只负责给已有行补单号，不负责建行）。
4. **写入 Base**：对命中的行 `+record-upsert --record-id "<rec_id>" --json '{"Tracking Info": "<追踪号>", "Carrier": "<carrier>"}' --as user`。
   - `Carrier`：**执行工单本身不存物流渠道**。优先取发货管理该行已有的 `logisticschannel_name`（原始值不 normalize）；
     若发货管理里该行也没有渠道 → 留空

…(truncated)
