# MCP 实时数据管道 v2.0（2026-07-26）

> 将销售工作台从 Excel 离线管道升级为 MCP 实时拉取，含完整后端 API + 前端三级降级 + 字段映射 + 自动刷新。

## 项目空间速查

| 空间 | project_key | simple_name | 用途 | 关键类型 |
|------|------------|-------------|------|---------|
| 销售管理 | `6593cd71471290e3cc6be6e6` | xsguanli | 客户/订单/活动/商机 | kh, xsdd, sj, story |
| 售后管理 | `658288abfb8bd616b17025f1` | br-shgl | **客诉（客户反馈单）** | **khts** (6669433056a98249604376de) |
| 客户项目 | `658bb60520ea78a2125f1b99` | csrw | 客户项目管理 | story |

> ⚠️ 客诉不在销售管理的 `issue` 表中！销售管理的 `issue` 永远为空。客诉在售后管理项目的 `khts`（客户反馈单）类型。

## MQL 查询规则

### 表名必须用完整 type_key
❌ `FROM project.kh` (api_name 不认)
✅ `FROM project.65ae1e403c87b152f3365ca6` (完整 type_key)

### 不支持 SELECT *
必须显式列出所有字段名。`SELECT *` 报 semantic error。

### 字段级权限
某些字段 MCP token 无权限读取（如 `field_ec1e8b` 行业），需逐个验证。

### 工作项类型映射
| 类型 | type_key | MQL表名 |
|------|----------|---------|
| 客户 | `65ae1e403c87b152f3365ca6` | `65ae1e403c87b152f3365ca6` |
| 销售订单 | `662612aea6bb7089fea044ef` | `662612aea6bb7089fea044ef` |
| 活动 | `65ae1e5d44338dbe7c39a29a` | `65ae1e5d44338dbe7c39a29a` |
| 商机 | `story` | `story` |
| **客诉（客户反馈单）** | `6669433056a98249604376de` | `6669433056a98249604376de` |

## MCP 响应解析

### 响应结构
```json
{
  "data": {"1": [{...50 items...}]},  // group_id → items
  "list": [{"group_infos": [...], "count": 1678}],  // 总数
  "session_id": "..."  // 翻页用
}
```

### moql_field_list 展开（完整版）
每条记录是 `moql_field_list` 数组，需展开为扁平 JSON。**必须处理三种值格式**：

```python
def flatten(item):
    flat = {}
    for f in item['moql_field_list']:
        k, v = f['key'], f['value']
        # 1. 单值 key_label_value（普通 select 字段）
        if isinstance(v, dict) and 'key_label_value' in v:
            flat[k] = v['key_label_value']['label']
        # 2. 数组 key_label_value（multi-select / workitem_related_multi_select）★
        elif isinstance(v, list):
            flat[k] = []
            for elem in v:
                if isinstance(elem, dict) and 'key_label_value' in elem:
                    flat[k].append({'key': elem['key_label_value']['key'], 'label': elem['key_label_value']['label']})
                elif isinstance(elem, dict) and len(elem) == 1:
                    flat[k].append(list(elem.values())[0])
                else:
                    flat[k].append(elem)
        # 3. 其他单值 dict（user 对象、date 等）
        elif isinstance(v, dict) and len(v) == 1:
            flat[k] = list(v.values())[0]
        # 4. 纯值（text、number、bool）
        else:
            flat[k] = v
    return flat
```

### 翻页
- 每页 50 条，`session_id` + `page_num` 翻页
- `list[0].count` = 总数，达到后停止
- 最后一页 items < 50

## 客诉字段解析（特殊处理）

客诉的 MCP 字段值格式与其他类型不同，前端 `adaptMcpFields` 需要特殊处理：

| 字段 | 实际格式 | 处理方法 |
|------|---------|---------|
| `work_item_status` | `[{key:"Wx3MJjKEZ", label:"技术支持信息补充中"}]` | 数组取 `[0].label` |
| `field_d107b0` | `true` (boolean) | 直接判断 `=== true` |
| `field_502a85` | `"是"` (string label) | 字符串比较 |
| `owner` | `{name_cn:"陈溪子", email:"...", user_key:"..."}` | 取 `.name_cn` |
| `current_status_operator` | `{name_cn:..., email:...}` 或数组 | 处理对象/数组 |
| `field_ac5caf` (客户) | `[{key:"4196185052", label:"广州诺瓦生物"}]` | 取 `[0].label` |

## 后端 API 架构

### 端点
```
GET  /api/mcp/{customers|orders|activities|complaints|opportunities}[?refresh=1]
POST /api/mcp/refresh  {"type": "all|customers|orders|..."}
```

### 缓存策略
- 客户: 30min TTL, 1700条/34页
- 订单: 5min TTL, 2500条
- 活动: 5min TTL, 2500条
- 客诉: 15min TTL, 350条
- 商机: 5min TTL

缓存同时写内存 + JSON 文件降级备份。

### JSON 备份文件命名（关键）
`set_mcp_cache` 写文件时，客户的 key 名必须是 `customers`（兼容 `renderCustomers`），其他类型用 `items`：
```python
top_key = 'customers' if cache_key == 'customers' else 'items'
json.dump({'updated_at': ..., top_key: data}, f, ...)
```
❌ 用 `data` 会导致客户列表 Tab 显示为空（代码期望 `mcp.customers`）。

## 前端 API 切换

### ENDPOINTS 格式
```javascript
var ENDPOINTS = [
  ['customers',    ['/api/mcp/customers',    '../数据/pricing/_key_customers.json']],
  ['orders',       ['/api/mcp/orders',       '../数据/orders_dashboard.json']],
  ['complaints',   ['/api/mcp/complaints',   '../数据/mcp_complaints.json']],
  // API路径为空 → 只用本地JSON
  ['kpiDashboard', ['',                       '../数据/kpi_dashboard.json']],
];
```

### 三级降级
```
Tunnel API (实时) → localhost API (本地直连) → 本地 JSON (离线备份)
```

### 自动刷新
```javascript
var AUTO_REFRESH_MS = 5 * 60 * 1000;
refreshTimer = setInterval(function() {
  if (document.visibilityState === 'visible') refreshData();
}, AUTO_REFRESH_MS);
```

## 搜索框修复模式（innerHTML 全量重建）★铁律

**问题**：innerHTML 全量重建导致 `<input>` 被销毁 → 失焦、光标跳末尾。

```javascript
// 1. 保存光标位置
var p = this.selectionStart;
orderState.filterText = this.value;

// 2. debounce 300ms（停手才触发）
debounce(function() {
  // 3. ★ 先 render() 重建 DOM
  render();
  // 4. ★ 再找新元素 + 恢复焦点（反了就白做！）
  var el = document.getElementById('orderSearchInput');
  if (el) { el.focus(); el.selectionStart = el.selectionEnd = p; }
}, 300)();
```
**关键顺序：render() → getElementById → focus。绝对不能 focus → render()。**

## 字段映射（MCP → 旧格式）

### 客户
| MCP字段 | 旧格式 | 
|---------|--------|
| `field_5ed7ab` | `dept` |
| `field_17186c` | `level` |
| `field_6415cf` | `location` |
| `field_2d5b6a` | `ctype` |
| `field_c3224b` | `group` |
| `field_e62869` | `products` |
| `field_79aa0b` | `opp_ids[]` |
| `work_item_id` | `work_item_id` / `fid` |

### 订单
| MCP字段 | 旧格式 |
|---------|--------|
| `name` | `orderId` |
| `field_2d6367` | `customer` |
| `field_a6e501` | `orderType` |
| `field_4a1b47` | `contractNo` |
| `field_2e095b` | `createDate` |
| `field_3a3ccb` | `product` |
| `field_8c4b7b` | `sku` |

### 客诉
| MCP字段 | 旧格式 | 备注 |
|---------|--------|------|
| `name` | `name` | 流程名称 |
| `work_item_id` | `id` | |
| `field_9e2144` | `area` | 反馈区域 |
| `field_3ce9fe` | `type` | 反馈/客诉类型 |
| `field_6fb810` | `type` 降级 | 产品线（类型为空时用） |
| `field_d107b0` | `priority` 判定 | 是否性能问题 |
| `field_502a85` | `priority` 判定 | 货值超5万=高 |
| `start_time` | `created` | 创建时间 |
| `days` | `days` | **前端计算**：`(now - created) / 86400000` |
| `finish_status` | `status` 判定 | true=已关闭 |

## 客户链接智能路由

```javascript
function feishuCustomerLink(c) {
  var cid = c.work_item_id || c.fid || '';
  var dept = (c.dept || '').toLowerCase();
  var isLS = dept.indexOf('生命科学') >= 0 || dept === '浙江' || dept === '广东';
  // 生命科学且有商机ID → 跳商机详情
  if (isLS && c.opp_ids && c.opp_ids.length > 0) {
    return 'https://project.feishu.cn/xsguanli/story/detail/' + c.opp_ids[0];
  }
  // 诊断原料 → 客户详情
  return cid ? 'https://project.feishu.cn/xsguanli/kh/detail/' + cid : '#';
}
```

## 飞书 URL 速查

| 空间 | 客户详情 | 商机详情 | 客诉详情 |
|------|---------|---------|---------|
| 销售管理 (xsguanli) | `/xsguanli/kh/detail/{id}` | `/xsguanli/story/detail/{id}` | — |
| 售后管理 (br-shgl) | — | — | `/br-shgl/khts/detail/{id}` |

## 已知限制

- `workitem_related_multi_select` 字段在 MQL 中常返回空（`field_79aa0b`），需用 `list_related_workitem` API 逐个查询
- 客户销售金额（total/sales_2024/25/26）MCP 不提供，需从 ERP 或订单聚合
- Cloudflare Tunnel 重启 URL 随机变化，需同步更新前端 `API_BASE_URLS` 和 `AUTH_URLS`
- 飞书桌面端嵌入浏览器不支持 CloudBase 匿名鉴权，需 OAuth 免登跳转
- `SELECT *` 和 api_name 别名在 MQL 中均不支持
- **客诉数据在独立项目空间**，不在销售管理的 `issue` 表中
