# MCP 数据管道模式与踩坑记录

## 分页方法

MCP `search_by_mql` 不支持 `count(*)`，但返回 `list[0].count` 显示总匹配数。

**正确分页**：`LIMIT 50 OFFSET N`，每页50条，OFFSET递增50。
- ❌ `group_pagination_list` 不可靠（返回重复数据）
- ✅ `LIMIT 50 OFFSET 0/50/100/150` 可靠无重复

```sql
SELECT ... FROM `销售管理`.`销售订单` WHERE ... ORDER BY `创建时间` DESC LIMIT 50 OFFSET 0
SELECT ... FROM `销售管理`.`销售订单` WHERE ... ORDER BY `创建时间` DESC LIMIT 50 OFFSET 50
```

## 直接调用 MCP Streamable HTTP 端点

当需要批量拉取大量数据时（如30+页），直接用 Python 调 MCP 端点比逐次通过 Hermes 工具调快 5-10 倍。

**端点**：`https://project.feishu.cn/mcp_server/v1`

**鉴权**：Header `X-Mcp-Token: m-abfb29e8-3104-434f-9944-8d0bb592f8cd`（长期token）

**流程**：
```python
# 1. Initialize
rpc({"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}})
# 返回 Mcp-Session-Id header，后续请求带此 header

# 2. Send initialized notification
rpc({"jsonrpc":"2.0","method":"notifications/initialized"})

# 3. Call tools
rpc({"jsonrpc":"2.0","id":N,"method":"tools/call",
     "params":{"name":"search_by_mql","arguments":{"project_key":"...","mql":"..."}}})
```

**响应格式**：SSE 包装 → 需要提取 `data:` 行，然后解析JSON。JSON 中 `result` 字段可能是字符串（需二次 `json.loads`）或对象（直接使用）。`result` 字符串尾部可能带 `\nlog_id: ...`（需截断）。

**完整脚本模板**：见 `脚本/shipment_pipeline.py` 和 `/tmp/fetch_money_batch2.py`（session-level scripts）。

## 字段提取规则

| MCP value_type | 提取方式 |
|---|---|
| `string_value` | `value.string_value` |
| `key_label_value` | `value.key_label_value.label` |
| `key_label_value_list` | 逗号连接各 `label` |
| `user_value` | `value.user_value.name_cn` (注意是 `name_cn` 非 `name`!) |
| `user_value_list` | 逗号连接各 `name_cn` |
| `double_value` | `value.double_value` 或 `float(value.string_value)` |
| `number` (type) | 同上 |

## 角色字段查询

飞书项目的角色字段用 `__角色名` 语法在 MQL 中查询：

```sql
-- 查询下单人角色
SELECT `物流单号`, `__下单人` FROM `销售管理`.`销售订单`
-- 返回值: user_value_list，每个元素含 name_cn/email/user_key
```

**常见角色**：`下单人`(role_00a9b7)、`销售员`(role_7dd6e0)、`客户-所属人`(role_9d3509)

**⚠️ 不要用 `创建者`(owner) 字段**——它是系统记录创建人（通常指向自动化机器人），不是实际业务人员。

## 订单合并策略：按单据编号而非物流单号（⚠️ 关键）

飞书销售订单一个单据可能含多行物料，每行有独立金额，共享单据编号和物流单号。

- ❌ 按物流单号去重只取首行 → 多行物料金额严重偏低（327万 vs 实际2200万+）
- ✅ 按**单据编号**(`orderId`)合并，同单据多行 → 金额/价税合计/数量累加
- 去掉 `物流单号 is not null` 过滤（无物流订单也纳入统计）
- 无物流单号订单标记 `hasTracking=false`, `shipment.status="no_tracking"`
- 顺丰API只对有物流单号的订单调用

## MCP 会话管理（⚠️ 高频踩坑）

- 单个 session 约 33 页后过期 → 返回空 JSON
- ❌ 单个 session 拉 63 页 → 中途丢失数据，覆盖目标文件为 0 条
- ✅ 每 20 页重建 session：`sid = init_session()`
- ✅ 写入前强制备份：`shutil.copy2(OUTPUT, OUTPUT + ".bak")`
- ✅ JSON 解析用 `json.JSONDecoder().raw_decode(content.strip())` 代替 regex
- ⚠️ k3 delegate_task MCP 拉取受配额限制（HTTP 403），大量拉取用终端直跑脚本

## 顺丰 API 调用

- 必须传 `checkPhoneNo`（手机号后4位），不传静默返回空路由
- `trackingNumber` 必须是数组 `["SFxxx"]`，`trackingType` 必须是数字 `1`
- 生产环境用 accessToken，数字签名返回 A1006
- 每 30 批刷新 token（2小时过期），每批 10 单号间隔 0.3s

## 数据分层架构：H1归档 + H2实时 + 自动归档

2026全量4128条价税合计2303.9万，全部实时拉取太重（8MB/5分钟）。分层后H2仅343条，日刷新30秒。

### 分层策略

```
H1（1-6月）→ h1_archive.json（3KB纯汇总）
H2（7月+）→ shipment_status.json（实时明细 + 顺丰轨迹）
已签收>7天 → 自动从H2归档到H1
```

### 归档规则

| 条件 | 操作 |
|------|------|
| 创建日期 ≤ 6/30 | → H1汇总 |
| 创建日期 > 6/30 且 签收日期 ≤ 7天前 | → H1汇总 |
| 创建日期 > 6/30 且 未签收/签收≤7天 | → H2实时 |

### 文件结构

```python
# h1_archive.json (3KB)
{
  "summary": {"total_orders": 3785, "total_tax": 21546000, ...},
  "monthly": {"2026-01": {...}, "2026-02": {...}, ...},
  "top_customers": [...],
  "departments": {...}
}

# shipment_status.json (仅H2实时)
{
  "orders": [...343条...],       # H2未归档订单
  "h1_summary": {...},           # 内嵌H1汇总指标
  "full_year_tax": 23039000,     # 全年价税合计
}

# orders_dashboard.json (1KB, 看板用)
{"h1": {...}, "h2": {...}, "full_year": {...}}
```

### 签收日期提取

顺丰路由的 `acceptTime` 字段（格式 `YYYY-MM-DD HH:MM:SS`）取最后一条 route：

```python
routes = o["shipment"].get("routes", [])
if routes:
    delivery_date = routes[-1].get("acceptTime", "")[:10]  # "2026-07-17"
```

### 看板MCP切换

看板4个概览卡片从ERP改MCP：

| 卡片 | 来源 | 计算方式 |
|------|------|---------|
| 客户数 | MCP订单 | `Object.keys(custSet).length` |
| 2026价税合计 | shipments.full_year_tax | 直接读取 |
| 本月价税 | H2本月订单 | `sum(taxTotal) where createDate starts with monthPrefix` |
| 联系人数 | MCP订单 | `Object.keys(contactSet).length`（收货人去重） |

KPI/月度趋势/三年对比保留ERP数据。

Python `SimpleHTTPRequestHandler` 默认只给 `.json` 和 `/api/` 路径加 `Cache-Control`。修改 `.html` 文件后浏览器可能用旧缓存。

**修复**：在 `end_headers()` 中加上 `.html`：
```python
if path.endswith('.json') or path.startswith('/api/') or path.endswith('.html'):
    self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
```
