# baoyu-workbench-ops — data pipeline

### ⚠️ 大 JSON 文件解析
`mcp_complaints.json`（~135KB）和 `shipment_status.json`（~1.6MB）**禁止用 read_file 全量读入上下文**。必须用 `terminal` Python 脚本解析，仅提取所需字段打印到 stdout。

## CloudBase 部署 Pitfalls


### 数据归档

```bash
# 归档未引用的分析 JSON
mkdir -p 数据/_archive
mv 数据/unused_file.json 数据/_archive/
```

当前已归档 41 个文件到 `_archive/`，78→37 个 JSON，释放 ~12MB。可随时恢复。


### ⚠️ 新增销售员数据补充

新销售员入驻后 `_key_customers.json` 初始为 0。需从 `diag_customers.json` 提取并合并：
```python
python3 << 'PYEOF'
import json
# 从 diag_customers.json 提取该人客户 → 合并入 _key_customers.json
# 然后运行 merge_workbench_data.py → tcb hosting deploy
PYEOF
```
详见 `references/data-source-authority.md`。

## 🔴 USER_AREA_MAP / USER_DEPT_MAP 陷阱

登录后 `state.user.dept` 是全称（如 `诊断原料大客户销售部`），但映射表常只配简称（`大客户部`）→ 全部 fallback 失败 → 客诉/客户过滤静默失效。

```javascript
// ❌ 错误 — 只配简称，全称全部 miss
var USER_AREA_MAP = {'大客户部':'大客户', '销售拓展部':'销售拓展部'};

// ✅ 正确 — 同时配简称和全称
var USER_AREA_MAP = {
  '大客户部':'大客户', '销售拓展部':'销售拓展部',
  '诊断原料大客户销售部':'大客户', '诊断原料销售拓展部':'销售拓展部',
  '诊断原料销售部':'销售拓展部'
};\n```\n\n## 🔴 KPI 达成率\"累计达成\"数据源陷阱\n\n看板 KPI 环形图下方\"累计达成\"对所有非管理员显示相同数字（全公司汇总，如 2353.7 万）。根因：`renderDashboard()` 中 `fullYearTax` 取自公司级 `dash.total_h1 + prog.total`。\n\n```javascript\n// ❌ 错误 — 所有人看到同一个公司汇总\nvar fullYearTax = (dash.total_h1 || 0) + (prog.total || 0);\n\n// ✅ 正确 — 管理员用公司汇总，其他人用个人 KPI\nvar kpi = getKPI();\nif (isAdmin) fullYearTax = (dash.total_h1 || 0) + (prog.total || 0);\nelse fullYearTax = (kpi.ytd || 0) + (kpi.current || 0);\n```\n\n## 🔴 客诉飞书链接 `work_item_id` vs `id` 字段名陷阱

**症状**：客诉 tab 所有飞书链接跳转到 `https://project.feishu.cn/br-shgl/khts/detail/undefined`。

**根因**：`mcp_complaints.json` 中客诉数据字段是 `work_item_id`，但 `index.html`/`app.html` 渲染用了不存在的 `c.id` → `undefined`。

**修复**：使用 fallback 模式兼容两套数据源：
```javascript
// ✅ MCP 数据用 work_item_id，api_server 用 id，两者兼容
'<a href="https://project.feishu.cn/br-shgl/khts/detail/' + (c.work_item_id || c.id || '') + '" ...>'
```

**影响范围**：`工具/index.html` L2207 + `工具/app.html` L1538，两处都必须同时修改。修复后需 `tcb hosting deploy` 部署到 CloudBase。

**预防**：所有从 MCP 直出的 JSON（如 `mcp_complaints.json`、`mcp_customers.json`）字段名沿用飞书 API 原始命名（`work_item_id`），前端渲染时必须确认实际字段名而非假设 `id` 存在。

## 🔴 AUTH[state.user] 迁移陷阱

老登录 `state.user` 是字符串，新 CloudBase 登录是对象。`AUTH[state.user]` / `state.user === 'gm'` 全部失效。用 `getUserName()` / `isAdminUser()` 统一适配。

## 数据源权威性\n\n| 用途 | 权威源 | 勿用 |\n|------|--------|------|\n| 销售金额 | `kpi_dashboard.json` | `shipment_status.json`（仅物流） |\n| 订单数 | `diag_kpis.json.orders` | `shipment_status.json`（仅发货） |\n| 月度明细 | `kpi_dashboard.json.h1_months` | — |\n| 客户清单 | `_key_customers.json` | — |

## Tab数据新鲜度检查

当用户问"哪些tab数据没更新"时，见 `references/tab-data-freshness.md`。

核心方法：`ls -la 数据/*.json` → 对比今日日期 → 标注滞后天数。
过期模式：商机沙盘(手动导入,最易过期) > MCP客户/客诉(Chron易断) > 活动/课题组(按需)。
KPI 8月需额外检查个人数据是否全为0。

## 数据上传 Tab（2026-08-07 新增）

见 `references/file-upload-tab.md`。完整流程：上传 → 质检(列名识别+校验) → 处理(ERP→KPI更新→merge→deploy) → 归档。

**🔴 openpyxl `read_only=True` 陷阱**：
```python
# ❌ read_only=True 时 iter_rows 返回 ReadOnlyCell → 遍历异常
next(ws.iter_rows(min_row=1, max_row=1))[0]  # 'ReadOnlyCell' is not iterable

# ✅ 去掉 read_only，直接用 values_only=True
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))]
```
QC 和处理都不能用 `read_only=True`。`values_only=True` 直接返回值，无需 `.value`。

**🔴 ERP 合计行过滤**：
```python
bn = str(row.get(bn_col, '') or '').strip()
if not bn or bn == 'None' or '合计' in bn:
    continue  # ⚠️ str(None)='None'，必须显式过滤
```

**服务托管（2026-09-04 起 launchd，不再是 tmux/裸启动）**：服务由 launchd 托管（`~/Library/LaunchAgents/ai.hermes.workbench.plist`），开机自启(RunAtLoad) + 崩溃自重启(KeepAlive, ThrottleInterval 30s) + 优雅停止(ExitTimeOut 25s)。
```bash
launchctl list | grep workbench                      # 看状态/PID
launchctl kickstart -k gui/$(id -u)/ai.hermes.workbench   # 改代码后重启
launchctl unload ~/Library/LaunchAgents/ai.hermes.workbench.plist   # 停止
# 日志：~/.hermes/logs/workbench.log（正常） / workbench.error.log（报错）
```
关键：服务跑 hermes venv python（`~/.hermes/hermes-agent/venv/bin/python3`，含 pymupdf 等依赖），plist 里 PATH 复用 gateway 的完整 PATH（覆盖 tesseract/textutil/python3 等 subprocess 调用）。改代码后用 kickstart 即可，无需 kill+裸起。验证 KeepAlive：`kill -9 <pid>` 后 sleep 12，lsof 应见新 PID 重新 LISTEN。


### 多用户服务化（已上线：本机 Mac 服务 + 外部用户上传）

上传通道：`api.biorisales.site:8443` → 本机 frpc 隧道 → `销售工作台服务.py`（:8765）；接口 `/api/upload`、`/api/uploads`、`/api/upload/process`（**均需 token**，未登录返回「登录已过期」）。

**已落地（报销助手 P1+P2 为范本；报销侧细节见 `baoyu-expense-assistant`）**：工作台「报销」Tab → frpc → 本机服务 → 后台工人 → 飞书通知本人；**一人一工作区** `报销助手/用户/<uid>/` + 共享区 `报销助手/共享/`；白名单 `报销助手/用户/_index.json`（`allow:1` 才放行，否则 403）；`user_roles` 已锁库（`read:false/write:false`，见 `baoyu-workbench-auth`）。

**通用配方（下一个要开放的处理管线照这 6 条做）**
1. **一人一工作区 + 子进程环境变量定位**：根路径由 env（如 `BAOYU_EXPENSE_HOME`）决定，处理时按用户 `subprocess` 起进程并设好 env —— **模块级全局路径不能在进程内可靠重定向**，串行复用同一进程必然串数据。
2. **HTTP 只落盘排队**：请求只写 `_jobs.json` 并返回 job id，重活（OCR/解析/打包）交单工人线程串行执行（OCR 吃 CPU），避免并发互踩与请求超时。
3. **按用户过滤的下载必须做路径校验**：`abspath(path).startswith(home + os.sep)`，否则 `?name=../..` 可越权读别人文件。
4. **空文件名不要退化成默认值**：`_safe_name('')` 返回 `'unnamed.pdf'` 会让「取最新产物」拿到旧文件/别人的文件 → 空名保持空。
5. **含个人信息的原始件（发票/身份信息）只存本机**，不上 CDN 公开目录；下载一律走带 token 接口，token 不进 URL。
6. **Mac 是处理引擎**：frpc 只保证网络通路，**不保证算力在线** —— 睡眠/关机即不处理（上传只排队）。上线前必须向用户交代这条硬依赖。

**上线后必跑的回归断言**：无 token → 401；非白名单账号 → 403；`pack?name=../../数据/...` → 非 200；两个账号的台账集合不重叠。改权限、改白名单、改服务后都重跑一次。

**上传ERP月份智能检测**：见 `references/smart-month-detection.md`。从单据编号前缀ZHBR2607→2026-07识别月份，当前月→kpi_progress，历史月→three_year_monthly+kpi_dashboard。

## 数据刷新定时策略

见 `references/refresh-cron-schedule.md`。每日 00:00/09:00/12:00/15:00/21:00/22:00 多时段覆盖 KPI+顺丰+MCP数据+CloudBase同步。
⚠️ MCP订单数据不可靠（测试单/预测单金额为0），KPI必须用ERP。
