# baoyu-workbench-mcp-pipeline — details

### ⚠️ 别用 `pgrep -f python3` 判断后台拉取是否存活（2026-09-21 实测）

本机 Python 是 Homebrew Python.app，进程名在 `ps` 里显示为 **`Python`**，不是 `python3`。`pgrep -fl "python3 (_pull_segmented|pull_orders)"` 返回空 → 会误判进程已死、文件未更新，进而重复启动脚本（并发写同一 JSON）。
正确判断：`pgrep -fl _pull_segmented` / `pgrep -fl pull_orders`（匹配脚本名，进程名无关），或 `ps -p <pid> -o pid,etime,command`。
另：21:00 这轮刷新与 job `6910c308a1db`（顺丰路由刷新-晚9点，cron 表达式就是 `0 21 * * *`，不是文档旧写的 21:09）**同在 21:00 启动**，其 `sf_delivery_watchdog.py` 会自己跑 `pull_orders.py --months 2` 并重写 `shipment_status.json`。两轮订单拉取会先后覆盖同一文件——本轮实测两次结果一致（1353 单）、无 JSON 损坏，但**merge 前必须先等 watchdog 进程退出**（`pgrep -f "sf_delivery_watchdog|shipment_pipeline"` 为空），否则可能读到半截 `shipment_status.json`。


### ⚠️ 大表深度 OFFSET 超时会「半截写文件」且仍报 ✅（2026-08-17 血泪教训）

`_full_refresh_offset.py` 的 `mcp_call` 用 `timeout=60` 且**无重试**。拉大表（客户 1699 条需 34 页）时，单次 `read operation timed out` 会直接 `break` 中断整个拉取，但仍**写出半截文件**（客户只拉到 1250/1699）并打印「✅ 写入 N 条」、脚本 exit 0 —— 表面全绿实则客户数据缺漏 ~450 条。

**排查**：Step 汇总行 `✅ N 条` 的 N 必须接近首屏 `list[0].count` 的 ~总数。客户表 ~1699，若 N≈1250 就是第 26 页（OFFSET=1250）超时截断，日志里可见 `❌ HTTP error: The read operation timed out`。

**修复**：
1. 逐页 `mcp_call` 加重试（`timeout=120` + 3 次重试），不要单次 60s 无重试就 break。
2. 超时后不要立即覆盖旧文件——先确认拉全再写（或写临时文件比对条数后替换）。
3. 补拉脚本已落地：`数据/_refill_customers.py`（单客户表带重试版，改 MQL/type_key 可复用）。

**铁律**：刷新完成后核对各 Step 最终条数 vs 历史基准，异常偏低先怀疑超时截断，不要信「✅」。

**2026-09-18 基准（四条并发后台 ~7 分钟）**：客诉 `pull_complaints.py` 342、客户 `_pull_segmented.py customers` 1735（129 段）、活动 `_pull_segmented.py activities --days 80` 5327（6月154/7月1867/8月2016/9月1290）、订单 `pull_orders.py --months 2` 1274 单（995 唯一物流单号，7 段 311~461 行）。⚠️ `_recover_pull_offset.mcp_call` 已内建 5 次重试（间隔 5/10/20/40s，非 JSON 内容也重试），日志出现单行 `⚠️ content text 非JSON` / `⚠️ 请求异常` 属**瞬时失败已自愈**——只有 `❌` 行才代表真丢数据；判断截断仍以「分段条数 < 相邻段 70%」为准。

**2026-09-21 21:00 基准（四条并发后台）**：客诉 346（creator 归并校验：黄明月/张立娅=0、刘子研=36）、客户 1738、活动 5200（窗口 07-03 起）、订单 1353 单（1069 唯一物流单号、51 张 FO 预测单；8 段 311~460 行）；`shipment_status.total`=1353、`workbench_data.shipments.total`=5412（H1 4063 + H2 1353）。

**2026-09-20 21:00 基准（四条并发后台，客诉2.5min/订单1.5min/活动5.8min/客户11.5min）**：客诉 343（creator 归并校验：黄明月/张立娅=0、刘子研=36）、客户 1737、活动 5208（窗口 07-02 起；月分布 7月1796/8月2016/9月1396）、订单 1313 单（1039 唯一物流单号、49 张 FO 预测单；8 段 311~460 行）。`shipment_status.total` = 订单数 1313、`workbench_data.shipments.total` = 5372（H1 4063 + H2 1313）为正确值。

**历史基准 2026-09-20 12:00**：客诉 342、客户 1737、活动 5149（7月1796/8月2016/9月1337）、订单 1289 单（996 唯一物流单号）。

⚠️ **判断后台拉取是否存活别看 `ps -p <启动时回显的 pid>`**（那是 `nohup ... &` 的包装 shell，秒退；python 子进程仍在跑）。用 `pgrep -f "_pull_segmented.py customers"` 判断，脚本结束以日志出现 `✅ 写入 ...: N 条` 为准。误判会导致**重复并发启动同一脚本**（本次跑了两份 customers，均成功写 1737 条，`.tmp`+条数校验闸兜住了冲突）。`setsid` 在本机 zsh 不存在，别用它脱离会话。**活动总数随窗口起点漂移，别当截断**：`--days 80` 起点每天后移，最早那 1~2 天会掉出（本次比 09-18 的 5327 少 178 = 6月154 + 7月初两天），分解到月即可对平；判断截断只看「某段条数 < 相邻段 70%」。晚于基准日数天的刷新条数只应小幅上浮。

**2026-09-14 基准（历史参考）**：客诉 339、客户 1729、活动 5110、订单 1120 单（841 唯一物流单号）。**保留的有用经验：分段脚本整体较慢**——客户表 2024-04 起按 7 天切段 ≈127 段，各表单跑约 6 分钟，**四个拉取（客诉+客户+活动+订单）建议 `terminal(background=true)` 并发跑**（互不冲突），单次 foreground timeout 600s 会不够；每跑完用 `process_manage(action='wait')` 收（wait 窗口被夹到 180s，需多轮 poll）。

**活动段 7/3~7/10 仅 ~190 条属真实数据非截断**（相邻周 400~535）：该周活动量天然偏低，与上一轮刷新的月合计一致，不要为它重拉。建议给 `scripts/cron_refresh_offset.py` 的 `mcp_call` 补上重试参数。


### 三级降级
```
Tunnel API → localhost API → 本地 JSON 文件
```


### ⚠️ 关键限制

- MCP 客户表**没有**销售金额字段。金额通过 `_key_customers.json`（ERP口径）交叉匹配。
- MCP 客诉 `list_todo` 方式不稳定（依赖 Token 用户待办）。改用 MQL 直接查询 `售后管理.客户反馈单`，全量 313 条。
- `workitem_related_multi_select` 字段在 MQL 中返回空，需单独查询关联类型。

## 日报系统（2026-07-30）

9板块日报结构 → `references/daily-report-architecture.md`
KPI cron: 每日00:00自动刷新+部署（job b5c6c96b1eee）
日报推送: 飞书 App cli_aaee6a2f90b89bc1 → 刘新元

## 已知问题与修复


### Python `double_value` 0 被 `or ''` 吞噬（2026-07-27 血案）

**`下单人` 和 `销售员` 不是普通字段，是工作项角色（role）。MQL SELECT 查不到，MCP 字段列表不包含它们。**


### 获取方式

```python
# ① 查角色 ID
list_workitem_role_config(project_key='6593cd71471290e3cc6be6e6',
                          work_item_type='662612aea6bb7089fea044ef',
                          role_query='下单人')
# → role_00a9b7 = 下单人, role_7dd6e0 = 销售员

# ② 按订单查角色成员
get_workitem_brief(fields=['role_7dd6e0'], work_item_id='7059668451')
# → role_members[].members[].name = 陈溪子
```


### Python `double_value` 0 被 `or ''` 吞噬（2026-07-27 血案）


### 前端重复函数声明
- **症状**：页面白屏/一直加载中，无 JS 报错
- **根因**：`patch` 操作在旧代码和新代码之间留下了两份 `function refreshData()` 声明
- **修复**：删除重复声明，`grep "function refreshData"` 确认只有一份


### 🔴 openpyxl `read_only=True` 导致只读一列（2026-08-07）
**症状**：质检显示文件"未识别，1 列"（实际 27 列）
**根因**：`read_only=True` 模式下 `iter_rows()` 返回 ReadOnlyCell，直接遍历只取到第一个 cell。
**修复**：质检用 `data_only=True`（不加 `read_only`），处理也用 `values_only=True`。
**标准模式**：
```python
wb = openpyxl.load_workbook(fp, data_only=True)
headers = [str(c) for c in next(ws.iter_rows(min_row=1, max_row=1, values_only=True))]
```
- **症状**：搜索框每次按键失焦，光标跳末尾
- **根因**：`oninput="...render()"` 触发全页 DOM 重建，输入框被销毁后重建
- **修复**：
  1. 用 `debounce(fn, 300)()` 防抖
  2. 先 `render()` 再 `focus()` + `selectionStart` 恢复
  3. 不要先 focus 后 render（focus 的是旧 DOM）
